基本操做能解決的問題,沒必要勞煩機器學習

前幾天讀MacTalk轉載的一篇文章《SQL能解決的問題,別動不動整機器學習》,文章認爲在新技術如機器學習、區塊鏈、虛擬現實等興起的今天,那些看似老舊的技術,反而有着歷久彌新的魅力。算法

我讀後有所感悟,Machine Learning確實很Cool,很Geek,可是在解決一些實際問題的時候,也許並非最好的解決方案。再好比有一組漫畫,問:爲何要作微服務?答:我也不知道,只是別人都在作。這固然只是一種漫畫式的戲謔,而此時由微服務須要引入分佈式事務,增長了系統的複雜度與成本。此時再反觀,這些業務真的須要這樣的過分設計嗎?古語有云:殺雞焉用牛刀?動用航母去運輸農副產品,顯然不大合適。bash

不過問題一分爲二,站在學習者的角度來講,多去了解學習先進技術,是大有裨益的。網絡

以前老大交給我一個小需求,問題是這樣的:如今有上萬個文本文件,每一個文件有幾千行文本,其中數字(幀數),空行,字符串(臺詞)。需求是對文本中的字符串進行必要去重,將幀數與臺詞匹配做爲參數push到某接口以及其餘的一些操做。數據結構

文本局部: 併發

如今我面對的主要是兩個問題,一個是如何較爲有效地對數據進行清洗;二是數據量有點大,須要併發。機器學習

先來思考第一個問題,因爲整個小系統是用Java寫的,我也沒有數據清洗的經驗,當時就先想着能不能用Java解決,假如不合適,再去查詢有沒有一些合適的Python輪子來處理這些。我先準備問問旁邊算法崗的老哥有沒有好的解決方式,他看了後說先對空行進行消除,而後能夠對相鄰的句子進行語義正確度的計算(大概是這麼說的,我不太記得術語了)留下值高的那一句等等。分佈式

頓時我有一種問題複雜了的感受,也考慮了其可行性,首先有沒有現成的接口給我調用?百度Ai開放平臺確實有,可是確定有次數限制,我不可能每條字符串都去調用一下接口。並且發送請求等待響應,而後再處理響應這塊的效率確定很低。微服務

我大體翻閱了一下臺詞字符串,這些是OCR識別出來的,在正片的時候,因爲底下只有一行,因此識別效果不錯,而在片頭片尾,因爲有大量演員職員信息與片頭片尾曲歌詞在屏幕上,因此合成的字符串較爲混亂。工具

重頭戲必然在正片部分,片頭片尾能夠另行處理,何況在同一部劇的狀況下,因爲片頭尾屬於重複部分,甚至能夠只處理一份,後面進行復制覆蓋便可(不像日本動漫會更換op/ed)性能

咱們來梳理一下,要解決哪些問題?

1. 獲取全部文件的路徑

解決方式:

本身編寫一個文件工具類,傳入根路徑,使用遞歸來獲取全部文件路徑,保存在一個ArrayList中。因爲我事先知道有接近一萬個文件,而ArrayList在進行動態擴容時是比較消耗性能的,因此最好在定義的時候就初始化其長度。

代碼:

public static List<String> getAllFilePath(String path) {
    ArrayList<String> fileList = new ArrayList();
    File file = new File(path);
    if (file.exists()) {
      File[] files = file.listFiles();
      if (files == null || files.length == 0) {
        return fileList;
      }
      for (File f : files) {
        if (f.isDirectory()) {
          fileList.addAll(getAllFilePath(f.getAbsolutePath()));
        } else {
          fileList.add(f.getPath());
        }
      }
    }
    return fileList;
  }
複製代碼

2. 文件較多,總體所佔空間較大,所有讀入內存再處理不合理

使用流來逐行讀入處理。

代碼:

public List<XXData> parserFile(String path) throws IOException, NoSuchFieldException, IllegalAccessException {
    File file = new File(path);
    BufferedReader br = new BufferedReader(new FileReader(file));
    while ((content = br.readLine()) != null) {
    // dosomething
    }
複製代碼

3. 對特殊符號、空白行進行初步過濾。

使用一些基本的正則來過濾。

4. 如何相鄰字符串進行類似度比較?

因爲識別出來的臺詞仍是比較規則的,對於這個類似度計算的要求其實比較低。我這裏用的是Levenshtein這個算法,來計算兩個字符串之間的編輯距離,以此來比較字符串之間的類似度。

public class Levenshtein {
  private int compare(String str, String target) {
    int d[][];
    int n = str.length();
    int m = target.length();
    int i;
    int j;
    char ch1;
    char ch2;
    int temp;
    if (n == 0) {
      return m;
    }
    if (m == 0) {
      return n;
    }
    d = new int[n + 1][m + 1];
    for (i = 0; i <= n; i++) {
      d[i][0] = i;
    }
    for (j = 0; j <= m; j++) {
      d[0][j] = j;
    }
    for (i = 1; i <= n; i++) {
      ch1 = str.charAt(i - 1);
      for (j = 1; j <= m; j++) {
        ch2 = target.charAt(j - 1);
        if (ch1 == ch2 || ch1 == ch2 + 32 || ch1 + 32 == ch2) {
          temp = 0;
        } else {
          temp = 1;
        }
        d[i][j] = min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + temp);
      }
    }
    return d[n][m];
  }

  private int min(int one, int two, int three) {
    return (one = one < two ? one : two) < three ? one : three;
  }

  /**
   * 獲取兩字符串的類似度
   */
  public float getSimilarityRatio(String str, String target) {
    return 1 - (float) compare(str, target) / Math.max(str.length(), target.length());
  }
}
複製代碼

5. 因爲須要將幀數與對應臺詞匹配,那麼選擇map這種數據結構來暫時存儲是比較方便的。可是選用哪一種Map呢?

HashMap不會維護插入順序,因爲須要獲取上一條數據與當前數據進行類似度比較,因此不合適。那麼LinkedHashMap呢?它是基於鏈表,擁有有序性,可是它的遍歷時間複雜度是O(N),太慢了,能不能直接獲取尾節點呢?可使用反射來獲取運行時信息,直接獲取尾節點,複雜度O(1)。當時也考慮過TreeMap,有序,基於紅黑樹,查找的時間複雜度爲O(logN)。因此咱們選擇的是LinkedHashMap。

代碼:

public class ActorLineFileParser {
  private static final Logger logger = LoggerFactory.getLogger(ActorLineFileParser.class);
  public List<Data> parserFile(String path) throws IOException, NoSuchFieldException, IllegalAccessException {
    File file = new File(path);
    BufferedReader br = new BufferedReader(new FileReader(file));
    String content = "";
    Map<Integer, String> map = new LinkedHashMap<>();
    Pattern pattern = Pattern.compile("[0-9]*");
    Integer time = 0;
    Levenshtein lt = new Levenshtein();
    while ((content = br.readLine()) != null) {
      if (content.length() > 0) {
        if (pattern.matcher(content).matches()) {
            time = Integer.parseInt(content);
            continue;
        }
        if (time > headEndPoint && time < tailStartPoint ) {
          Field tail = map.getClass().getDeclaredField("tail");
          tail.setAccessible(true);
          Map.Entry<Integer, String> previousLine = (Map.Entry<Integer, String>) tail.get(map);
          content = content.trim().replaceAll(",|,|%|【|[a-z]|\\?|%|\\/|《|》", "");
          if (previousLine == null) {
            map.put(time, content);
            continue;
          }
          float similarityRatio = lt.getSimilarityRatio(content, previousLine.getValue());
          if (similarityRatio > 0.49) {
            map.put(previousLine.getKey(), content);
          } else {
            map.put(time, content);
          }
        }
      }
    }
    br.close();
    // 後續操做省略
  }
}

複製代碼

6. 將數據處理封裝好後,須要進行push到某接口,而且持久化記錄

這些操做是比較耗時的,那就須要開啓多個線程來進行工做。個人思路是,每將一個文本中的內容清洗封裝完成後,就將這個實體(一個包含了多條字幕信息的列表)加入一個集合,而後對這個實體進行其餘的一些驗證以及進一步封裝等等的操做(這裏就不展開了),最後將其加入一個任務隊列,等待線程池的分配資源,進行網絡傳輸與持久化。

這裏須要提醒的是,記得爲線程池的阻塞隊列設置長度以及拒絕規則。不然一直是核心線程在工做,並不會動用設置的最大線程。同時打好日誌,方便查看運行狀況。

我忘記設置阻塞隊列長度,結果一直只有8個核心線程在工做,致使這個程序運行了30多個小時才結束。

若是你有耐心讀到這裏,我也但願你不要曲解個人本意。我並非要唱衰機器學習、區塊鏈這些新技術,它們是利劍,正在被這個世界上最優秀的人才鍛造,互聯網時代的新階段須要它們去披荊斬棘。可是在有些狀況下,使用基本操做,能得到低成本、高效率的結果。由於這些如今看似尋常的技術,也曾是最優秀。

相關文章
相關標籤/搜索