今天周末休息,娱乐娱乐在看NLP,NLP中最基本的过程之一是分词,也是构建LM并训练词向量的基础。
这里面有几个问题,第一,就是分词问题,这里我们不提IK,JieBa这些中文分词器,我们只去讨论leetcode中的题目,从简单的动态规划入手,到bigram,这是第一部分
第二,比如说一个词和另一个词的相似程度,比如app,apple,apply的相似程度我以怎样的方式去表示。最简单的一种方式就是比如今天要说的编辑距离。比如我认为apple变为apply需要编辑1个字符,而apple变为app需要改变两个字符,那么apple和apply的相似程度就比apple和app要高。这leetcode中也有类似题目,对应的直接就是编辑距离
所以再有人跟你说,刷leetcode没有用,那么你一定得清楚,刷leetcode是为了打好算法的基础,因为在此基础上构建的生产算法,是你的目标,你连基础都搞不掂,还搞个屁?你刷以为没用,那一定是你学的不够多,因为学的越多的人,都觉得自己越无知,学的越少的人,往往觉得自己什么都知道。
进入正题:
1,分词
给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
示例 1:
输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。
示例 2:输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。
注意,你可以重复使用字典中的单词。
做dp题,是有套路的,大致过程得有:确定dp含义,画理想中的dp结果,写dp方程,确定边界条件,debug实现
确定dp含义,dp[i]表示前i个字母是否可以被词典分
那么对于s = "leetcode", wordDict = ["leet", "code"],其dp数组可以写出来
dp[] = {0,0,0,1,0,0,0,1},为了方便写作00010001,
那么dp方程
dp[i] = dp[i-k]&&words.contains(subString(i-k,i)),k表示截取单词长度
eg:leetcode 那么对于dp[i],k表示从0到i截取字符串
对dp[3],也就是lee需要从,substring(0,1),substring(0,2),substring(0,3)判断,到底dict里有没有这个串,有就true,没有就false
综上,代码
public static boolean wordBreak(String s, List<String> wordDict) {
boolean[] dp = new boolean[s.length() + 1];
for (int i = 0; i <= s.length(); i++) {
for (int k = 0; k <= i; k++) {
String substring = s.substring(i - k, i);
if (k == i) {
if (wordDict.contains(s.substring(0, i))) {
dp[i] = true;
}
}else {
dp[i] = dp[i - k] && wordDict.contains(substring);
}
if (dp[i]) {
break;
}
}
}
return dp[dp.length - 1];
}
2、编辑距离
给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
示例 1:
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
示例 2:输入:word1 = "intention", word2 = "execution"
输出:5
解释:
intention -> inention (删除 't')
inention -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')
public int minDistance(String word1, String word2) {
if (word1.length() == 0 || word2.length() == 0) {
return word1.length() + word2.length();
}
int length1 = word1.length() ;
int length2 = word2.length() ;
int[][] dp = new int[length1 + 1][length2 + 1];
for (int i = 0; i <= length1; i++) {
dp[i][0] = i;
}
for (int i = 0; i <= length2; i++) {
dp[0][i] = i;
}
for (int i = 1; i <= length1; i++) {
for (int j = 1; j <= length2; j++) {
if (word1.charAt(i-1) == word2.charAt(j-1)) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = Math.min(Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1), dp[i - 1][j - 1] + 1);
}
}
}
return dp[length1][length2];
}
不会的同学,可以直接去leetcode看解答,我为什么想把编辑距离放在这有两点:
1、动规在NLP里有应用场景,而且不少,所以别觉得动规没有应用场景
2、针对动规本身而言,两个单词,往往都是构造dp[i][j],i表示第一个单词的前i个字母,j表示第二个单词的前j个字母
3、Bigram分词
给出第一个词 first 和第二个词 second,考虑在某些文本 text 中可能以 "first second third" 形式出现的情况,其中 second 紧随 first 出现,third 紧随 second 出现。
对于每种这样的情况,将第三个词 "third" 添加到答案中,并返回答案。
示例 1:
输入:text = "alice is a good girl she is a good student", first = "a", second = "good"
输出:["girl","student"]
示例 2:输入:text = "we will we will rock you", first = "we", second = "will"
输出:["we","rock"]
Bigram是词法的基础,一般unigram是指单个单词,Trigram是指三个单词下一个,leetcode只是抛砖引玉,就这道题而言,做法很多,穷举就可以了,我这里也只是提出一个问题供大家思考:Bigram的训练集,为Bigram提供了得到第三个单词的基础,假设我们的训练集足够大,按理说能够包含所有的语言所拥有的情况,我们是不是可以通过前两个单词,预测下一个单词的走向,那我们是不是又可以通过三个单词构造一个句子?但是问题来了,怎么样一个句子才算通顺,怎么样才符合人的思维,这才是NLU关注的点,也就是词的特性——词向量。NLP的核心就是围绕词向量展开的,由于我也是新手上路,就不献丑了,留个大家自己学。
代码很简单,穷举就完事了:
class Solution {
public String[] findOcurrences(String text, String first, String second) {
String[] words = text.split(" ");
List<String> list = new ArrayList<String>();
for (int i = 2; i < words.length; i++) {
if (words[i - 2].equals(first) && words[i - 1].equals(second)) {
list.add(words[i]);
}
}
int size = list.size();
String[] ret = new String[size];
for (int i = 0; i < size; i++) {
ret[i] = list.get(i);
}
return ret;
}
}
但愿我有一天有能力和有时间来把NLP这个大坑给大家填一填