2020/08/31

8D問題解決法 Eight Disciplines Problem Solving

8D問題解決法Eight Disciplines Problem Solving8D)也稱為團隊導向問題解決方法8D Report,是一個處理及解決問題的方法,常用於品質管理。

8D問題解決法的目的是識別一再出現的問題,並且要矯正並消除此問題,有助於產品及製程的提昇。若條件許可時,8D問題解決法會依照問題的統計分析來產生問題的永久對策,並且用確認根本原因的方式聚焦在問題的根源。

8D問題解決法是在汽車產業、電子組裝業界及其他產業中,利用團隊方式結構性徹底解決問題時的標準作法,常被用在回覆客戶的投訴案件報告上。

8D 是八個作業程序,品管人員依照步驟,就能夠找出問題發生的原因,並將分析問題的過程提供給客戶,同時驗證解決問題的方法,並防止問題再度發生。

8D 的起源

8D最早是美國福特公司使用。

二戰期間,美國政府率先採用一種類似8D的流程——「軍事標準1520」,又稱之為「不合格品的修正行動及部署系統」。

1987年,福特汽車公司最早以文件記錄下8D法,在其一份課程手冊中,這個方法被命名為「團隊導向的問題解決法」(Team Oriented Problem Solving)。 當時,福特的動力系統部門正被一些經年累月、反覆出現的生產問題搞得焦頭爛額,因此其管理層提請福特集團提供指導課程,幫助解決難題。

8D 的應用

  • 找出不合格的產品問題的原因及解決方法
  • 處理客戶投訴的問題的原因及解決方法
  • 分析反覆發生的問題的原因及解決方法

8D 的目標

  • 提升解決問題的效率,累積經驗
  • 提升產品品質
  • 避免或減少問題反覆發生
  • 找到問題的發生原因,提出短期,中期和長期對策並採取相應行動措施
  • 跨部門組成處理小組,改進產品開發流程的品質,防止問題再次發生

8D 的工作步驟

8D是解決問題的8條基本準則或稱8個工作步驟,但在實際應用中卻有9個步驟:

  • D0:徵兆緊急反應措施

  • D1:小組成立

  • D2:定義與描述問題

  • D3:確認、實施並確認臨時對策

  • D4:確認、識別及確認根本原因及漏失點(escape points)

  • D5:針對問題或不符合規格部份,選擇及確認永久對策

  • D6:實施永久對策

  • D7:採取預防措施

  • D8:感謝團隊成員


D0:徵兆緊急反應措施

目的:主要是為了看此類問題是否需要用 8D 來解決,如果問題太小,或是不適合用8D來解決的問題(例如價格,經費等等),這一步是針對問題發生時候的緊急反應。

關鍵:判斷問題的類型、大小、範疇等等。與D3不同,D0是問題一開始發生的反應,而D3是針對產品或服務問題本身的暫時對策。

D1:小組成立

目的:成立一個小組,小組成員具備 artifact/product 的開發知識,有足夠的時間及主導權,同時應具有所要求的能解決問題和實施對策的技術素質。小組必須有一個小組長。

關鍵:小組成員要有產品的開發知識,能夠解決問題

ex: 品管部(Quality Assurance):通常是小組召集人,負責統一回答客戶的問題。

製程部(Process):負責找出製程當中哪裡可能有問題。

測試部(Testing):尋找為何無法由測試方法檢出有問題的產品。

生產部(Production):配合工程師的要求,做實驗或收集數據,以利問題的發現並協助執行解決方案。

D2:定義與描述問題

目的:運用 5W1H (Who, What, Where, When, Why, How) 描述,來向團隊說明問題是在何時、何地、發生了什麽事、嚴重程度、目前狀態、如何緊急處理。

關鍵:搜集相關資料及數據,識別問題、確定範圍,跟客戶一起確認問題以及風險等級

D3:確認、實施並確認臨時對策

目的:保證在永久對策實施前,將問題與內外部顧客隔離。當問題發生時,不論原因找到與否,都必須要先止血,所以會先採取一些必要的暫時性措施。比如說如何在客戶端幫忙篩選(Screen, Sort)出有問題的產品,或者是更換良品給客戶,讓客戶可以繼續生產或是出貨。

在製造端應該要先採取措施防止問題產品繼續發生或出到客戶的手上,例如更換機器生產、加嚴篩選、全檢、將自動改爲手動、庫存清查等等。

暫時對策決定後,應立刻交由團隊成員帶回執行,並隨時回報成效。

關鍵:找到並選擇最佳的臨時對策,進行記錄與驗證(DOE、PPM分析、控製圖等)

D4:確認、識別及確認根本原因及漏失點(escape points)

目的:用統計工具列出可以所有潛在原因,將問題說明中提到的造成偏差的一系列事件或環境或原因相互隔離測試,並確定產生問題的根本原因。

最常使用的方法是【要因分析圖(魚骨圖)】,它提醒我們有哪些線索可以尋找,就人(員)、工(製程)、(來)料、機(器)、量(測)、及環(境)等六個面向,逐步檢討找出問題可能發生的原因,仔細比較、分析問題發生前後變動的狀況,比如說人員是否變動?作業手法是否更動?廠商來料是否變更?治具是否更換?跟環境的溫度、溼度是否相關?

經驗得知,日常作業的資料收集越齊全的工廠,找到真正問題的速度就越快,例如有日常修理報表,Cpk(統計製程),良率即時通報系統...等。

D5:針對問題或不符合規格部份,選擇及確認永久對策

目的:在生產前測試方案,並對方案進行評審以確定所選的校正對策能夠解決客戶問題,同時對其它過程不會有不良影響。

關鍵:驗證並決定最佳對策,如果有需要,就要重新評估臨時對策。將對策提交管理階層,確保能執行永久對策。

D6:實施永久對策

目的:制定一個實施永久對策的計劃,確定控制方法並納入文件,以確保消除了根本原因。在生產中應用該措施時應監督其長期效果。

關鍵:執行永久對策,廢除臨時措施。利用故障的可測量屬性,確認故障已經排除

D7:採取預防措施

目的:修改現有的管理系統、操作系統、工作慣例、設計與規程以防止這一問題與所有類似問題重覆發生。

關鍵:選擇預防措施,驗證有效性並進行監控

D8:感謝團隊成員

目的:承認小組的集體努力,對小組工作進行總結並祝賀。

關鍵:有選擇的保留重要文檔,將小組心得記錄到文件,必要的物質、精神獎勵。

References

8D問題解決法wiki

8-個解決問題的步驟

問題分析與對策解決,簡介8D report方法

8D工作方法

一步解決8D報告回復之痛

工廠8D報告

8D法是什麼?詳解8D法的九步驟!

2020/08/17

判斷點是否在多邊形內的方法

給定一個由多個點的 list 產生的多邊形,判斷另一個點座標,是否有包含在該多邊形的圖形中。

方法是從給定點座標開始,往隨便一個方向射出一條射線(例如水平往右射線),看看穿過多少條邊。如果穿過偶數次,表示點在簡單多邊形外部;如果穿過奇數次,表示點在簡單多邊形內部。

不過,要另外處理,當射線穿過頂點、射線與邊重疊的狀況,也就是給定點座標,與某一條邊共線的狀況。

這兩個連結有對方法做更詳細的說明

Point in Polygon

How to check if a given point lies inside or outside a polygon?

另外有提供一個 Java 版的實作

// A Java program to check if a given point
// lies inside a given polygon
// Refer https://www.geeksforgeeks.org/check-if-two-given-line-segments-intersect/
// for explanation of functions onSegment(),
// orientation() and doIntersect()
class GFG
{

    // Define Infinite (Using INT_MAX
    // caused overflow problems)
    static int INF = 10000;

    static class Point
    {
        int x;
        int y;

        public Point(int x, int y)
        {
            this.x = x;
            this.y = y;
        }
    };

    // Given three colinear points p, q, r,
    // the function checks if point q lies
    // on line segment 'pr'
    static boolean onSegment(Point p, Point q, Point r)
    {
        if (q.x <= Math.max(p.x, r.x) &&
            q.x >= Math.min(p.x, r.x) &&
            q.y <= Math.max(p.y, r.y) &&
            q.y >= Math.min(p.y, r.y))
        {
            return true;
        }
        return false;
    }

    // To find orientation of ordered triplet (p, q, r).
    // The function returns following values
    // 0 --> p, q and r are colinear
    // 1 --> Clockwise
    // 2 --> Counterclockwise
    static int orientation(Point p, Point q, Point r)
    {
        int val = (q.y - p.y) * (r.x - q.x)
                - (q.x - p.x) * (r.y - q.y);

        if (val == 0)
        {
            return 0; // colinear
        }
        return (val > 0) ? 1 : 2; // clock or counterclock wise
    }

    // The function that returns true if
    // line segment 'p1q1' and 'p2q2' intersect.
    static boolean doIntersect(Point p1, Point q1,
                               Point p2, Point q2)
    {
        // Find the four orientations needed for
        // general and special cases
        int o1 = orientation(p1, q1, p2);
        int o2 = orientation(p1, q1, q2);
        int o3 = orientation(p2, q2, p1);
        int o4 = orientation(p2, q2, q1);

        // General case
        if (o1 != o2 && o3 != o4)
        {
            return true;
        }

        // Special Cases
        // p1, q1 and p2 are colinear and
        // p2 lies on segment p1q1
        if (o1 == 0 && onSegment(p1, p2, q1))
        {
            return true;
        }

        // p1, q1 and p2 are colinear and
        // q2 lies on segment p1q1
        if (o2 == 0 && onSegment(p1, q2, q1))
        {
            return true;
        }

        // p2, q2 and p1 are colinear and
        // p1 lies on segment p2q2
        if (o3 == 0 && onSegment(p2, p1, q2))
        {
            return true;
        }

        // p2, q2 and q1 are colinear and
        // q1 lies on segment p2q2
        if (o4 == 0 && onSegment(p2, q1, q2))
        {
            return true;
        }

        // Doesn't fall in any of the above cases
        return false;
    }

    // Returns true if the point p lies
    // inside the polygon[] with n vertices
    static boolean isInside(Point polygon[], int n, Point p)
    {
        // There must be at least 3 vertices in polygon[]
        if (n < 3)
        {
            return false;
        }

        // Create a point for line segment from p to infinite
        Point extreme = new Point(INF, p.y);

        // Count intersections of the above line
        // with sides of polygon
        int count = 0, i = 0;
        do
        {
            int next = (i + 1) % n;

            // Check if the line segment from 'p' to
            // 'extreme' intersects with the line
            // segment from 'polygon[i]' to 'polygon[next]'
            if (doIntersect(polygon[i], polygon[next], p, extreme))
            {
                // If the point 'p' is colinear with line
                // segment 'i-next', then check if it lies
                // on segment. If it lies, return true, otherwise false
                if (orientation(polygon[i], p, polygon[next]) == 0)
                {
                    return onSegment(polygon[i], p,
                                     polygon[next]);
                }

                count++;
            }
            i = next;
        } while (i != 0);

        // Return true if count is odd, false otherwise
        return (count % 2 == 1); // Same as (count%2 == 1)
    }

    // Driver Code
    public static void main(String[] args)
    {
        Point polygon1[] = {new Point(0, 0),
                            new Point(10, 0),
                            new Point(10, 10),
                            new Point(0, 10)};
        int n = polygon1.length;
        Point p = new Point(20, 20);
        if (isInside(polygon1, n, p))
        {
            System.out.println("Yes");
        }
        else
        {
            System.out.println("No");
        }
        p = new Point(5, 5);
        if (isInside(polygon1, n, p))
        {
            System.out.println("Yes");
        }
        else
        {
            System.out.println("No");
        }
        p = new Point(-1, 10);
        n = polygon1.length;
        if (isInside(polygon1, n, p))
        {
            System.out.println("Yes");
        }
        else
        {
            System.out.println("No");
        }


        Point polygon2[] = {new Point(0, 0),
            new Point(5, 5), new Point(5, 0)};
        p = new Point(3, 3);
        n = polygon2.length;
        if (isInside(polygon2, n, p))
        {
            System.out.println("Yes");
        }
        else
        {
            System.out.println("No");
        }
        p = new Point(5, 1);
        if (isInside(polygon2, n, p))
        {
            System.out.println("Yes");
        }
        else
        {
            System.out.println("No");
        }
        p = new Point(8, 1);
        if (isInside(polygon2, n, p))
        {
            System.out.println("Yes");
        }
        else
        {
            System.out.println("No");
        }
    }
}

以下的實作,是根據 Java 版本的內容,改成 erlang 版

-module(polygon).

%% API
-export([
  test/0,
  test2/0
]).

-record(point, {
  x :: float(),
  y :: float()
}).
-type(point() :: #point{}).

-define(INF, 10000.0).

%% 檢查 Q 是否在 線段 PR 上
%% check if point Q lise on line segment(PR)
-spec on_segment(P :: point(), Q :: point(), R :: point() ) -> boolean().
on_segment(P, Q, R) ->
  #point{x = Px, y = Py} = P,
  #point{x = Qx, y = Qy} = Q,
  #point{x = Rx, y = Ry} = R,

%%  io:format("P=~p, Q=~p, R=~p~n", [P, Q, R]),
%%
%%  io:format("Qx=~p, max(Px, Rx)=~p~n", [Qx, max(Px, Rx)]),
%%  io:format("Qx=~p, min(Px, Rx)=~p~n", [Qx, min(Px, Rx)]),
%%  io:format("Qy=~p, max(Py, Ry)=~p~n", [Qy, max(Py, Ry)]),
%%  io:format("Qy=~p, min(Py, Ry)=~p~n", [Qy, min(Py, Ry)]),
  case (Qx =< max(Px, Rx)) and (Qx >= min(Px, Rx)) and( Qy =< max(Py, Ry)) and (Qy >= min(Py, Ry)) of
    true ->
      true;
    _ ->
      false
  end.

%% 查詢 P, Q, R 的順序
%% 0: 三點共線, 1: clockwise 順時鐘, 2: counterclockwise 逆時鐘
-spec orientation(P :: point(), Q :: point(), R :: point() ) -> integer().
orientation(P, Q, R) ->
  #point{x = Px, y = Py} = P,
  #point{x = Qx, y = Qy} = Q,
  #point{x = Rx, y = Ry} = R,

  Val = (Qy - Py) * (Rx - Qx) - (Qx - Px) * (Ry - Qy),
  case Val == 0 of
    true ->
      0;
    false ->
      case Val > 0 of
        true ->
          1;
        _ ->
          2
      end
  end.

%% 確認 line(P1, Q1) 是否有跟 line(P2, Q2) 相交
-spec intersect(P1 :: point(), Q1 :: point(), P2 :: point(), Q2 :: point() ) -> boolean().
intersect(P1, Q1, P2, Q2) ->
  Orientation1 = orientation(P1, Q1, P2),
  Orientation2 = orientation(P1, Q1, Q2),
  Orientation3 = orientation(P2, Q2, P1),
  Orientation4 = orientation(P2, Q2, Q1),

  % general case
  case (Orientation1 /= Orientation2) and (Orientation3 /= Orientation4) of
    true ->
      true;
    _ ->
      %% Special Cases
      %% p1, q1 and p2 are colinear and p2 lies on segment p1q1
      case (Orientation1 == 0) and on_segment(P1, P2, Q1) of
        true ->
          true;
        _ ->
          %% p1, q1 and p2 are colinear and q2 lies on segment p1q1
          case (Orientation2 == 0) and on_segment(P1, Q2, Q1) of
            true ->
              true;
            _ ->
              %% p2, q2 and p1 are colinear and  p1 lies on segment p2q2
              case (Orientation3 == 0) and on_segment(P2, P1, Q2) of
                true ->
                  true;
                _ ->
                  %% p2, q2 and q1 are colinear and q1 lies on segment p2q2
                  case (Orientation4 == 0) and on_segment(P2, Q1, Q2) of
                    true ->
                      true;
                    _ ->
                      false
                  end
              end
          end
      end
  end.

-spec in_polygon(Polygon :: list(), P :: point() ) -> boolean().
in_polygon(Polygon, P) ->
  case length(Polygon) < 3 of
    true ->
      false;
    _ ->
      #point{x = _Px, y = Py} = P,
      %% 產生一個點,最後要跟 P 連成一條 Py 到 INF 的水平線段
      PInf = #point{x=?INF, y=Py},

      %% 計算 line(P, PInf) 跟所有多邊形的邊線的交點的數量
      %% Count intersections of the above line with sides of polygon

      % 分成第一個點, 跟其他點 兩個 list
      {PolygonHead, PolygonLast} = lists:split(1, Polygon),

%%      io:format("PolygonHead=~p, PolygonLast=~p~n", [PolygonHead, PolygonLast]),

      % 把 第一個點接到 PolygonLast 後面
      Polygon2 = lists:append(PolygonLast, PolygonHead),
      % 合併 Polygon, Polygon2 為新的 list, [{Polygon, Polygon2}]
      PolygonList = lists:zip(Polygon, Polygon2),

%%      io:format("PolygonList=~p~n", [PolygonList]),

      {CountRes, OnSegmentFlagRes, OnSegmentRes} = lists:foldl(fun({P1, P2}, {Count, OnSegmentFlag, OnSegment}) ->

%%        io:format("  lists:foldl P1=~p, P2=~p, intersect(P1, P2, P, PInf)=~p, orientation(P1, P, P2)=~p, on_segment(P1, P, P2)=~p~n", [P1, P2, intersect(P1, P2, P, PInf), orientation(P1, P, P2), on_segment(P1, P, P2)]),
        %% 判斷 (P1, P2), (P, PInf) 是否有交點
        case intersect(P1, P2, P, PInf) of
          true ->
            case orientation(P1, P, P2) == 0 of
              true ->
                %% 如果 P 跟 line(P1, P2) 共線,判斷是否 P 有在該線段上
                {Count, true, on_segment(P1, P, P2)};
              _ ->
                {Count + 1, OnSegmentFlag, OnSegment}
            end;
          _ ->
            {Count, OnSegmentFlag, OnSegment}
        end
                                                               end,
        {0, false, false}, PolygonList),

%%      io:format("CountRes=~p, OnSegmentFlagRes=~p, OnSegmentRes=~p, (CountRes rem 2)=~p~n", [CountRes, OnSegmentFlagRes, OnSegmentRes, CountRes rem 2]),
      %% 判斷交點數量是否為奇數
      %% Return true if count is odd, false otherwise
      case OnSegmentFlagRes of
        true ->
          OnSegmentRes;
        _ ->
          case (CountRes rem 2) == 1 of
            true ->
              true;
            _ ->
              false
          end
      end
  end.

test() ->
  P = #point{x = 1.0, y = 2.0},
  Q = #point{x = 2.0, y = 4.0},
  R = #point{x = 3.0, y = 6.0},
  S = #point{x = 4.0, y = 8.0},

  ResOnSegment = on_segment(P, Q, R),
  io:format("ResOnSegment=~p~n", [ResOnSegment]),

  ResOrientation = orientation(P, Q, R),
  io:format("ResOrientation=~p~n", [ResOrientation]),

  ResIntersect = intersect(P, Q, R, S),
  io:format("ResIntersect=~p~n", [ResIntersect]),
  ok.

test2() ->
  Polygon = [ #point{x = 0.0, y = 0.0}, #point{x = 10.0, y = 0.0}, #point{x = 10.0, y = 10.0}, #point{x = 0.0, y = 10.0} ],

  Res1 = in_polygon(Polygon, #point{x = 20.0, y = 20.0}),
  io:format("Res1=~p~n", [Res1]),

  Res2 = in_polygon(Polygon, #point{x = 5.0, y = 5.0}),
  io:format("Res2=~p~n", [Res2]),

  Res3 = in_polygon(Polygon, #point{x = -1.0, y = 10.0}),
  io:format("Res3=~p~n", [Res3]),

  %%%%%%%%%%%
  Polygon2 = [ #point{x = 0.0, y = 0.0}, #point{x = 5.0, y = 5.0}, #point{x = 5.0, y = 0.0} ],
  Res4 = in_polygon(Polygon2, #point{x = 3.0, y = 3.0}),
  io:format("Res4=~p~n", [Res4]),

  Res5 = in_polygon(Polygon2, #point{x = 5.0, y = 1.0}),
  io:format("Res5=~p~n", [Res5]),

  Res6 = in_polygon(Polygon2, #point{x = 8.0, y = 1.0}),
  io:format("Res6=~p~n", [Res6]),

  ok.

2020/08/10

詞向量 Word Embedding

文章本身是一種非結構化的資料,無法直接被計算。word representation 就是將這種訊息轉化為結構化的資訊,這樣就可以針對 word representation 計算,完成文章分類、情緒判斷等工作。

word representation 的方法很多,例如:

  1. one-hot representation

    例如: 貓、狗、牛、羊 用向量中一個欄位來表示

    貓:[1,0,0,0]
    狗:[0,1,0,0]
    牛:[0,0,1,0]
    羊:[0,0,0,1]

    缺點是沒有辦法表示出詞語之間的關係,另外因為向量中大部分都是 0,稀疏的向量,導致計算及儲存的效率都很低

  2. integer representation

    都以一個整數來表示每一個詞,將詞語的整數連接成 list,就是一句話

    貓:1
    狗:2
    牛:3
    羊:4

    缺點是沒有辦法表示出詞語之間的關係

  3. word embedding

    可用較低維度的向量表示詞語,不像one-hot 那麼長。詞意相近的詞,在向量空間中的距離比較接近

    有兩種主流的 word embedding 方法

    • word2vec

      2013 年由 google 的 Mikolov 提出,該演算法有兩種模式:利用前後文來預測目前的詞語,或是利用目前的詞語預測前後文

    • GloVe (Global Vector for Word Representation)

      延伸了 word2vec

word2vec

  • CBOW (Continuous Bag-of-Words Model)

    利用前後文來預測目前的詞語,相當於一句話中扣掉一個詞,猜這個詞是什麼。

  • Skip-gram (Continuous Skip-gram Model)

    利用目前的詞語預測前後文,相當於給一個詞,猜前面和後面可能出現什麼詞。

    

ref: Word2Vec 的兩種模型:CBOW 與 Skip-gram

優點:

  1. 通用性佳,適合用在多種 NLP 問題上
  2. 比舊的 word embedding 方法的向量維度小,計算速度比較快

缺點:

  1. 由於詞和向量是一對一的關係,所以無法處理多義詞的問題
  2. word2vec 是一種靜態的表示方式,通用性強,但無法針對特定任務做動態優化

window

以 「孔乙己 一到 店 所有 喝酒 的 人 便都 看著 他 笑」 這一句話為例,去掉停用字後,會得到

孔乙己 一到 店 所有 喝酒 人 看著 笑

以 「人」 這個單詞為例,window =1 時,就是該單詞前後 1 格的另一個單詞,這樣會得到這樣的結果

喝酒 人 看著

電腦就能知道「人」跟「喝酒」「看著」有關係。

window 用來定義 word2vec 文章分析時,單詞前後關係的距離。

gensim

gensim 是使用 google 釋出的 word2vec 模型的套件,可找到字的向量、相似字,計算向量之間的相似度,WMDistance 可計算兩個句子之間的相似度。

取得 wiki 文章資料

以下以 維基百科 wiki zh data 下載的 20200301 中文版資料 zhwiki-20200301-pages-articles.xml.bz2 1.8 GB 做測試,注意我們要的是以 pages-articles.xml.bz2 結尾的備份。

先安裝 gensim

pip3 install gensim

gensim 已經有提供了 WikiCorpus,可以快速取得 wiki 文章的標題及內容。執行以下程式,會產生一個 wiki_texts.txt 文字檔,裡面是所有 wiki_corpus.get_texts() 取得的文章內容。

# -*- coding: utf-8 -*-
## wiki_to_txt.py

import logging
import sys

from gensim.corpora import WikiCorpus

def main():

    if len(sys.argv) != 2:
        print("Usage: python3 " + sys.argv[0] + " wiki_data_path")
        exit()

    logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)
    wiki_corpus = WikiCorpus(sys.argv[1], dictionary={})
    texts_num = 0

    with open("wiki_texts.txt",'w',encoding='utf-8') as output:
        for text in wiki_corpus.get_texts():
            output.write(' '.join(text) + '\n')
            texts_num += 1
            if texts_num % 10000 == 0:
                logging.info("已處理 %d 篇文章" % texts_num)

if __name__ == "__main__":
    main()

執行

python3 wiki_to_txt.py zhwiki-20200301-pages-articles.xml.bz2

執行結果

2020-03-30 15:27:59,410 : INFO : finished iterating over Wikipedia corpus of 356901 documents with 82295378 positions (total 3436353 articles, 97089149 positions before pruning articles shorter than 50 words)

斷詞

因為wiki 文章中,把簡體字跟繁體字混在一起了,先透過 OpenCC 進行簡體字轉繁體的處理

ref:

安裝 OpenCC

wget https://github.com/BYVoid/OpenCC/archive/ver.1.0.5.tar.gz -O opencc.1.0.5.tgz

tar -zxvf opencc.1.0.5.tgz
cd OpenCC-ver.1.0.5/

# 產生 Makefile
mkdir build
cd build

## CENTOS 執行以下命令
cmake -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=Release LE_GETTEXT:BOOL=ON  ..
## MAC 執行以下命令
# cmake -DCMAKE_INSTALL_PREFIX=/usr/local -DCMAKE_BUILD_TYPE=Release -D ENABLE_GETTEXT:BOOL=OFF  -DCMAKE_OSX_ARCHITECTURES=x86_64  ..

make
make install

sudo ln -s /usr/lib/libopencc.so.2 /usr/lib64/libopencc.so.2

## 測試
opencc --help
opencc --version

利用 opencc 將簡體字轉為繁體

opencc -i wiki_texts.txt -o wiki_zh_tw.txt -c s2tw.json

安裝 jieba

pip3 install jieba

測試

import jieba

seg_list = jieba.cut("我来到清华大学", cut_all=False)
print("Default Mode: " + "/ ".join(seg_list))

ithomeironman/day16NLP_Chinese/ 可下載一個繁體中文的字典 dict.txt.big,以及 停用字 stops.txt。將 jieba 改為使用繁體字典

# -*- coding: utf-8 -*-
## segment.py
import jieba
import logging

def main():

    logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)

    # jieba custom setting.
    jieba.set_dictionary('jieba_dict/dict.txt.big')

    # load stopwords set
    stopword_set = set()
    with open('jieba_dict/stops.txt','r', encoding='utf-8') as stopwords:
        for stopword in stopwords:
            stopword_set.add(stopword.strip('\n'))

    output = open('wiki_seg.txt', 'w', encoding='utf-8')
    with open('wiki_zh_tw.txt', 'r', encoding='utf-8') as content :
        for texts_num, line in enumerate(content):
            line = line.strip('\n')
            words = jieba.cut(line, cut_all=False)
            for word in words:
                if word not in stopword_set:
                    output.write(word + ' ')
            output.write('\n')

            if (texts_num + 1) % 10000 == 0:
                logging.info("已完成前 %d 行的斷詞" % (texts_num + 1))
    output.close()

if __name__ == '__main__':
    main()

執行要花 30 分鐘

# python3 segment.py
2020-03-30 15:50:14,355 : DEBUG : Prefix dict has been built successfully.
......
2020-03-30 16:17:47,295 : INFO : 已完成前 350000 行的斷詞

訓練單詞向量

Word2Vec 有許多參數

gensim.models.word2vec.Word2Vec(sentences=None, size=100, alpha=0.025, window=5, min_count=5, max_vocab_size=None, sample=0.001, seed=1, workers=3, min_alpha=0.0001, sg=0, hs=0, negative=5, cbow_mean=1, hashfxn=<built-in function hash>, iter=5, null_word=0, trim_rule=None, sorted_vocab=1, batch_words=10000)

比較常用的是

  • sentences:這是要訓練的句子集合
  • size:這是訓練出的詞向量會有幾維
  • alpha:機器學習中的學習率,這東西會逐漸收斂到 min_alpha
  • sg:sg=1表示採用skip-gram,sg=0 表示採用cbow
  • window:能往左往右看幾個字的意思
  • workers:執行緒數目
  • min_count:若這個詞出現的次數小於min_count,那他就不會被視為訓練對象
# -*- coding: utf-8 -*-

import logging

from gensim.models import word2vec

def main():

    logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)
    sentences = word2vec.LineSentence("wiki_seg.txt")
    model = word2vec.Word2Vec(sentences, size=250)

    #保存模型,供日後使用
    model.save("word2vec.model")

    #模型讀取方式
    # model = word2vec.Word2Vec.load("your_model_name")

if __name__ == "__main__":
    main()

模型測試

# -*- coding: utf-8 -*-

from gensim.models import word2vec
from gensim import models
import logging

def main():
    logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)
    model = models.Word2Vec.load('word2vec.model')

    print("提供 3 種測試模式\n")
    print("輸入一個詞,則去尋找前一百個該詞的相似詞")
    print("輸入兩個詞,則去計算兩個詞的餘弦相似度")
    print("輸入三個詞,進行類比推理")

    while True:
        try:
            query = input()
            q_list = query.split()

            if len(q_list) == 1:
                print("相似詞前 100 排序")
                res = model.most_similar(q_list[0],topn = 100)
                for item in res:
                    print(item[0]+","+str(item[1]))

            elif len(q_list) == 2:
                print("計算 Cosine 相似度")
                res = model.similarity(q_list[0],q_list[1])
                print(res)
            else:
                print("%s之於%s,如%s之於" % (q_list[0],q_list[2],q_list[1]))
                res = model.most_similar([q_list[0],q_list[1]], [q_list[2]], topn= 100)
                for item in res:
                    print(item[0]+","+str(item[1]))
            print("----------------------------")
        except Exception as e:
            print(repr(e))

if __name__ == "__main__":
    main()

測試

籃球
相似詞前 100 排序
美式足球,0.6760541796684265
排球,0.6475502848625183
橄欖球,0.6430544257164001
男子籃球,0.6427032351493835
冰球,0.6138877272605896
棒球,0.6081532835960388
籃球隊,0.6004550457000732
足球,0.5992617607116699
.....

----------------------------
電腦 程式
計算 Cosine 相似度
0.5263175
----------------------------
衛生紙 啤酒
計算 Cosine 相似度
0.3263663
----------------------------
衛生紙 面紙
計算 Cosine 相似度
0.70471543
----------------------------

電腦 程式 電視
電腦之於電視,如程式之於
電腦系統,0.6098563075065613
程式碼,0.6063085198402405
軟體,0.5896543264389038
電腦程式,0.5740373730659485
終端機,0.5652117133140564
計算機程序,0.5597981810569763
除錯,0.554024875164032
計算機,0.549680769443512
作業系統,0.543748140335083
直譯器,0.5432565212249756
介面,0.5425338745117188
......

自然語言處理的應用

簡單

  • 拼寫檢查 ( Spell Checking )
  • 關鍵字搜索 ( Keyword Search )
  • 尋找同義詞 ( Finding Synonyms )

  • 從網頁和文檔解析信息 ( Parsing information from websites, documents, etc. )

複雜

  • 機器翻譯 ( Machine Translation )
  • 語義分析 ( Semantic Analysis )
  • 指代詞分析 ( Coreference ), 例如,”he” 和”it” 在文檔中指誰或什麼?
  • 問答系統 ( Question Answering )

References

詞向量詳解:從word2vec、glove、ELMo到BERT

Word2vec

詞嵌入 | Word embedding

自然語言處理入門- Word2vec小實作

讓電腦聽懂人話: 直觀理解 Word2Vec 模型

Gensim Word2Vec 簡易教學

產品標籤分群實作-Word2Vec

以 gensim 訓練中文詞向量

實作Tensorflow (5):Word2Vec

2020/08/03

中文斷詞

在中文自然語言處理NLP中,要對一堆文字詞語組成的文章進行分析, 分析前要先拆解文章,也就是斷詞,我們要分析的對象是詞語,而不是一個一個中文字,這跟英文完全不同,因為英文的斷詞就直接用標點符號、空白去區隔即可。

目前繁體中文斷詞系統有 中研院 CKIP 以及 jieba,在一些舊的文章中都提到 jieba 無法適當地處理繁體中文,而有替換繁體中文字典的改進作法,不過目前 jieba 已經到了 0.42 版,以下先了解官方的套件的功能,再看看需不需要修改繁體中文字典。

jieba 演算法

  • 基於前綴詞典實現高效的詞圖掃描,生成句子中漢字所有可能成詞情況所構成的有向無環圖 (DAG)
  • 採用了動態規劃查找最大概率路徑,找出基於詞頻的最大切分組合
  • 對於未登錄詞,採用了基於漢字成詞能力的 HMM 模型,使用了 Viterbi 算法

安裝

可直接用 pip 安裝,或是將 jieba source code 的 jieba 目錄放在目前的工作目錄,或是 site-packages 目錄中

如果要使用 paddle 的分詞語詞性標注功能,必須安裝 paddlepaddle-tiny

pip3 install paddlepaddle-tiny==1.6.1

先直接下載 source code 試試看

wget https://github.com/fxsjy/jieba/archive/v0.42.1.tar.gz -O jieba-0.41.1.tgz

tar zxvf jieba-0.41.1.tgz

virtual environemnt

virtualenv --system-site-packages /root/venv-jieba
source /root/venv-jieba/bin/activate

## 如果不使用 paddlepaddle,這兩個套件也可以不安裝
pip3 install numpy==1.16.4
pip3 install paddlepaddle-tiny==1.6.1

# 把 soruce code 中的 jieba 目錄移動到工作目錄中
mv ~/temp/download/jieba-0.41.1/jieba ~/temp

斷詞

有四種斷詞模式

  • 精確模式,試圖將句子最精確地切開,適合文本分析
  • 完整模式,把句子中所有的可以成詞的詞語都掃描出來, 速度非常快,但是不能解決歧義;
  • 搜索引擎模式,在精確模式的基礎上,對長詞再次切分,提高召回率,適合用於搜索引擎分詞。
  • paddle模式,利用PaddlePaddle深度學習框架,訓練序列標注(雙向GRU)網絡模型實現分詞。同時支持詞性標注。paddle模式使用需安裝 paddlepaddle-tiny

函式

  • jieba.cut 方法接受四個輸入參數: 需要分詞的字符串;cutall 參數用來控制是否採用全模式;HMM 參數用來控制是否使用 HMM 模型;usepaddle 參數用來控制是否使用paddle模式下的分詞模式,paddle模式採用延遲加載方式,通過enable_paddle接口安裝paddlepaddle-tiny,並且import相關代碼
  • jieba.cutforsearch 方法接受兩個參數:需要分詞的字符串;是否使用 HMM 模型。該方法適合用於搜索引擎構建倒排索引的分詞,粒度比較細
  • 待分詞的字符串可以是 unicode 或 UTF-8 字符串、GBK 字符串。注意:不建議直接輸入 GBK 字符串,可能無法預料地錯誤解碼成 UTF-8
  • jieba.cut 以及 jieba.cutforsearch 返回的結構都是一個可迭代的 generator,可以使用 for 循環來獲得分詞後得到的每一個詞語(unicode),或者用 jieba.lcut 以及 jieba.lcutforsearch 直接返回 list
  • jieba.Tokenizer(dictionary=DEFAULT_DICT) 新建自定義分詞器,可用於同時使用不同詞典。jieba.dt 為默認分詞器,所有全局分詞相關函數都是該分詞器的映射。

# encoding=utf-8
import jieba

print()
print("完整模式:")
seg_list = jieba.cut("肺炎疫情的挑戰日益嚴峻,新竹清華大學自農曆年起陸續已採取了量測體溫等全面的防疫措施。", cut_all=True)
print("Full Mode: " + "/ ".join(seg_list))  # 全模式

print()
print("精確模式:")
seg_list = jieba.cut("肺炎疫情的挑戰日益嚴峻,新竹清華大學自農曆年起陸續已採取了量測體溫等全面的防疫措施。", cut_all=False)
print("Default Mode: " + "/ ".join(seg_list))  # 精確模式

print()
print("預設是精確模式:")
seg_list = jieba.cut("肺炎疫情的挑戰日益嚴峻,新竹清華大學自農曆年起陸續已採取了量測體溫等全面的防疫措施。")  # 預設是精確模式
print(", ".join(seg_list))

print()
print("搜索引擎模式:")
seg_list = jieba.cut_for_search("肺炎疫情的挑戰日益嚴峻,新竹清華大學自農曆年起陸續已採取了量測體溫等全面的防疫措施。")  #
print(", ".join(seg_list))

print()
print("Paddle Mode:")
jieba.enable_paddle()# 啓動paddle模式。 0.40版之後開始支持,早期版本不支持
strs=["肺炎疫情的挑戰日益嚴峻,新竹清華大學自農曆年起陸續已採取了量測體溫等全面的防疫措施。","乒乓球拍賣完了","新竹清華大學"]
for str in strs:
    seg_list = jieba.cut(str,use_paddle=True) # 使用paddle模式
    print("Paddle Mode: " + '/'.join(list(seg_list)))

執行結果

完整模式:
Building prefix dict from the default dictionary ...
Loading model from cache /tmp/jieba.cache
Loading model cost 0.433 seconds.
Prefix dict has been built successfully.
Full Mode: 肺炎/ 疫情/ 的/ 挑/ 戰/ 日益/ 嚴/ 峻/ ,/ 新竹/ 清/ 華/ 大/ 學/ 自/ 農/ 曆/ 年/ 起/ 陸/ 續/ 已/ 採/ 取/ 了/ 量/ 測/ 體/ 溫/ 等/ 全面/ 的/ 防疫/ 措施/ 。

精確模式:
Default Mode: 肺炎/ 疫情/ 的/ 挑戰/ 日益/ 嚴峻/ ,/ 新竹/ 清華大學/ 自農/ 曆/ 年/ 起/ 陸續/ 已/ 採取/ 了/ 量/ 測體/ 溫/ 等/ 全面/ 的/ 防疫/ 措施/ 。

預設是精確模式:
肺炎, 疫情, 的, 挑戰, 日益, 嚴峻, ,, 新竹, 清華大學, 自農, 曆, 年, 起, 陸續, 已, 採取, 了, 量, 測體, 溫, 等, 全面, 的, 防疫, 措施, 。

搜索引擎模式:
肺炎, 疫情, 的, 挑戰, 日益, 嚴峻, ,, 新竹, 清華大學, 自農, 曆, 年, 起, 陸續, 已, 採取, 了, 量, 測體, 溫, 等, 全面, 的, 防疫, 措施, 。

Paddle Mode:
W0327 11:45:31.179752 21561 init.cc:157] AVX is available, Please re-compile on local machine
Paddle enabled successfully......
Paddle Mode: 肺炎/疫情/的/挑戰/日益/嚴/峻,新竹清華大學自農曆年起陸續/已/採取/了/量測體溫/等/全面/的/防疫/措施/。
Paddle Mode: 乒乓/球拍/賣/完/了
Paddle Mode: 新竹/清華大學
  • jieba.lcut() lcut(),意思跟cut()是一樣的,只是返回的型態變成list

# encoding=utf-8
import jieba

print()
print("完整模式:")
seg_list = jieba.lcut("肺炎疫情的挑戰日益嚴峻,新竹清華大學自農曆年起陸續已採取了量測體溫等全面的防疫措施。", cut_all=True)
print("Full Mode: ", seg_list)  # 全模式


print()
print("精確模式:")
seg_list = jieba.lcut("肺炎疫情的挑戰日益嚴峻,新竹清華大學自農曆年起陸續已採取了量測體溫等全面的防疫措施。", cut_all=False)
print("Default Mode: ", seg_list)  # 精確模式

print()
print("預設是精確模式:")
seg_list = jieba.lcut("肺炎疫情的挑戰日益嚴峻,新竹清華大學自農曆年起陸續已採取了量測體溫等全面的防疫措施。")  # 預設是精確模式
print(seg_list)

print()
print("搜索引擎模式:")
seg_list = jieba.lcut_for_search("肺炎疫情的挑戰日益嚴峻,新竹清華大學自農曆年起陸續已採取了量測體溫等全面的防疫措施。")  #
print(seg_list)

print()
print("Paddle Mode:")
jieba.enable_paddle()# 啓動paddle模式。 0.40版之後開始支持,早期版本不支持
strs=["肺炎疫情的挑戰日益嚴峻,新竹清華大學自農曆年起陸續已採取了量測體溫等全面的防疫措施。","乒乓球拍賣完了","新竹清華大學"]
for str in strs:
    seg_list = jieba.lcut(str,use_paddle=True) # 使用paddle模式
    print("Paddle Mode: ", seg_list)

自訂詞典

  • 雖然 jieba 有新詞識別能力,但是自行添加新詞可以保證更高的正確率

  • 用法: jieba.loaduserdict(filename)

    file_name 為文件類對象或自定義詞典的路徑

  • 詞典格式和 dict.txt 一樣,一個詞佔一行;每一行分三部分:詞語、詞頻(可省略)、詞性(可省略),用空格隔開,順序不可顛倒。file_name 若為路徑或二進制方式打開的文件,則文件必須為 UTF-8 編碼。

  • 詞頻省略時使用自動計算的方式處理,能保證分出該詞的詞頻。

  • 使用 addword(word, freq=None, tag=None) 和 delword(word) 可在程序中動態修改詞典。

  • 使用 suggest_freq(segment, tune=True) 可調節單個詞語的詞頻,使其能(或不能)被分出來。

  • 注意:自動計算的詞頻在使用 HMM 新詞發現功能時可能無效。

在專案路徑下新增一個檔案叫做:userdict.txt

內容如下:

農曆年
量測
體溫
日益嚴峻

可在程式一開始,就載入自訂詞典

jieba.load_userdict('userdict.txt')

執行結果

精確模式:
Default Mode:  ['肺炎', '疫情', '的', '挑戰', '日益嚴峻', ',', '新竹', '清華大學', '自', '農曆年', '起陸續', '已', '採取', '了', '量測', '體溫', '等', '全面', '的', '防疫', '措施', '。']

可動態調整詞語的頻率

jieba.suggest_freq(('陸續'), True)
print()
print("精確模式2:")
seg_list = jieba.lcut("肺炎疫情的挑戰日益嚴峻,新竹清華大學自農曆年起陸續已採取了量測體溫等全面的防疫措施。", cut_all=False)
print("Default Mode: ", seg_list)  # 精確模式

執行結果

精確模式2:
Default Mode:  ['肺炎', '疫情', '的', '挑戰', '日益嚴峻', ',', '新竹', '清華大學', '自', '農曆年', '起', '陸續', '已', '採取', '了', '量測', '體溫', '等', '全面', '的', '防疫', '措施', '。']

也可以直接替換詞典

ithomeironman/day16NLP_Chinese/ 可下載一個繁體中文的字典 dict.txt.big

# encoding=utf-8
import jieba

jieba.set_dictionary('dict.txt.big')
# with open('stops.txt', 'r', encoding='utf8') as f:
#     stops = f.read().split('\n')

print()
print("精確模式:")
seg_list = jieba.lcut("肺炎疫情的挑戰日益嚴峻,新竹清華大學自農曆年起陸續已採取了量測體溫等全面的防疫措施。", cut_all=False)
print("Default Mode: ", seg_list)

關鍵詞抽取

TF-IDF 方法

# jieba.analyse.extract_tags(sentence, topK=20, withWeight=False, allowPOS=())
# sentence 為待提取的文本
# topK 為返回幾個 TF/IDF 權重最大的關鍵詞,默認值為 20
# withWeight 為是否一並返回關鍵詞權重值,默認值為 False
# allowPOS 僅包括指定詞性的詞,默認值為空,即不篩選
# jieba.analyse.TFIDF(idf_path=None) 新建 TFIDF 實例,idf_path 為 IDF 頻率文件

TextRank 方法

jieba.analyse.textrank(sentence, topK=20, withWeight=False, allowPOS=('ns', 'n', 'vn', 'v')) 直接使用,接口相同,注意默認過濾詞性。
jieba.analyse.TextRank() 新建自定義 TextRank 實例
# -*- coding: utf-8 -*-
import jieba
import jieba.analyse

jieba.set_dictionary('dict.txt.big')

print()
text = '肺炎疫情的挑戰日益嚴峻,新竹清華大學自農曆年起陸續已採取了量測體溫等全面的防疫措施。'
tags = jieba.analyse.extract_tags(text, topK=10)
print(tags)
# ['挑戰', '嚴峻', '清華大學', '農曆年', '陸續', '採取', '測體溫', '防疫', '新竹', '肺炎']

print()
print('textrank:')
for x, w in jieba.analyse.textrank(text, withWeight=True):
    print('%s %s' % (x, w))

# textrank:
# 日益 1.0
# 全面 1.0
# 肺炎 0.6631715416020616
# 防疫 0.6631715416020616
# 疫情 0.6605033585768562
# 措施 0.6605033585768562
# 新竹 0.3607120276929184
# 了量 0.3607120276929184

詞性標注

ref: 彙整中文與英文的詞性標註代號:結巴斷詞器與FastTag / Identify the Part of Speech in Chinese and English

結巴預設會將標點符號標示為「x」,而不是「w」。而且英文會被標示為「eng」

# -*- coding: utf-8 -*-
import jieba
import jieba.posseg as pseg

text = '肺炎疫情的挑戰日益嚴峻,新竹清華大學自農曆年起陸續已採取了量測體溫等全面的防疫措施。'
seg_list = pseg.lcut(text)
# print("Default Mode: ", seg_list)

for word, flag in seg_list:
    print("", word, " : ", flag)

執行結果

 肺炎  :  n
 疫情  :  n
 的  :  uj
 挑戰  :  vn
 日益  :  n
 嚴峻  :  a
 ,  :  x
 新竹  :  ns
 清華大學  :  nt
 自  :  p
 農  :  ng
 曆  :  zg
 年  :  q
 起  :  v
 陸  :  nr
 續  :  v
 已  :  d
 採  :  v
 取  :  v
 了  :  ul
 量  :  n
 測  :  v
 體  :  ng
 溫  :  v
 等  :  u
 全面  :  n
 的  :  uj
 防疫  :  vn
 措施  :  n
 。  :  x

word cloud

參考這篇文章中文自然語言處理基礎 以及資源

下載檔案:

cloud_mask7.png
sumsun.ttf
stops.txt

安裝其它套件

pip3 install collections
pip3 install wordcloud
pip3 install matplotlib
yum -y install python-imaging
import jieba
jieba.set_dictionary('dict.txt.big')  # 如果是使用繁體文字,請記得去下載繁體字典來使用
with open('stops.txt', 'r', encoding='utf8') as f:
    stops = f.read().split('\n')

text = "肺炎疫情的挑戰日益嚴峻,新竹清華大學自農曆年起陸續已採取了量測體溫等全面的防疫措施。"

from collections import Counter
from wordcloud import WordCloud
from matplotlib import pyplot as plt

stops.append('\n')  ## 換行符號,加入停用字中,可以把它拿掉
stops.append('\n\n')
terms = [t for t in jieba.cut(text, cut_all=True) if t not in stops]

sorted(Counter(terms).items(), key=lambda x:x[1], reverse=True)  ## 這個寫法很常出現在Counter中,他可以排序,list每個item出現的次數。


plt.clf()
wordcloud = WordCloud(font_path="simsun.ttf")  ##做中文時務必加上字形檔
wordcloud.generate_from_frequencies(frequencies=Counter(terms))
plt.figure(figsize=(15,15))
plt.imshow(wordcloud, interpolation="bilinear")
plt.axis("off")
plt.savefig('cloud1.png')


from PIL import Image
import numpy as np

alice_mask = np.array(Image.open("cloud_mask7.png"))  ## 請更改cloud_mask7.png路徑
wc = WordCloud(background_color="white", max_words=2000, mask=alice_mask, font_path="simsun.ttf")
wc.generate_from_frequencies(Counter(terms))  ## 請更改Counter(terms)

wc.to_file("cloud2.png")  ##如果要存檔,可以使用

# plt.clf()
# plt.imshow(wc, interpolation='bilinear')
# plt.axis("off")
# plt.figure()
# plt.imshow(alice_mask, cmap=plt.cm.gray, interpolation='bilinear')
# plt.axis("off")
# plt.savefig('cloud2.png')

中文斷詞.png

References

pypi jieba

jieba 原始 github

python-11-利用jieba實現中文斷詞

NLP 中文斷詞最方便的開源工具之一 —— Jieba

Python-知名Jieba中文斷詞工具教學

2017

結巴中文斷詞台灣繁體版本 APCLab

結巴中文斷詞台灣繁體版本 ldkrsi