2020/08/31

8D問題解決法 Eight Disciplines Problem Solving

8D問題解決法(Eight Disciplines Problem Solving,8D)也稱為團隊導向問題解決方法或 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