基本操做能解决的问题,没必要劳烦机器学习

前几天读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多个小时才结束。

若是你有耐心读到这里,我也但愿你不要曲解个人本意。我并非要唱衰机器学习、区块链这些新技术,它们是利剑,正在被这个世界上最优秀的人才锻造,互联网时代的新阶段须要它们去披荆斩棘。可是在有些状况下,使用基本操做,能得到低成本、高效率的结果。由于这些如今看似寻常的技术,也曾是最优秀。

相关文章
相关标签/搜索