【问题标题】:Word matching in corpus of text is very slow文本语料库中的单词匹配非常慢
【发布时间】:2014-06-26 16:00:47
【问题描述】:

我有两张桌子。在表 1 中,我有大约 400K 行,其中每行包含一段文本,最多可包含 50 个句子。在表 2 中,我有一个包含 80k 单词的词典,其中包含我对每个段落的每个单词进行编码所需的分数。

我的 php 脚本的重点是将每段文本分解为所需的多个单词,然后在词典中查找每个单词的分数,最后计算所有单词的总分每一行。

到目前为止,我的策略是编写一个执行以下操作的脚本:

  1. 连接数据库,表 1
  2. While Loop,一排接一排
  3. 对于当前行,展开段落。
  4. 对于每个单词,如果该单词存在,请查阅表 2 并返回分数。
  5. 以当前行的总分结束。
  6. 用当前段落的总分更新表 1。
  7. 回到第 2 点。

我的代码有效,但效率不高。问题是脚本太慢了,让它运行一个小时只计算前 500 行。这是一个问题,因为我有 400K 行。我将需要此脚本用于其他项目。

你会建议我做些什么来减少这个过程的紧张?

<?php

//Include functions
        include "functions.php";
        ini_set('max_execution_time', 9000000);
        echo 'Time Limit = ' . ini_get('max_execution_time');
        $db='senate';   
//Function to search into the array lexicon     
        function searchForId($id, $array) {
        foreach ($array as $key2 => $val) {
        if ($val['word'] === $id) {
        return $key2;
       } 
   }
   return null;
}       

// tags to remove
        $remove   = array('{J}','{/J}','{N}','{/N}','{V}','{/V}','{RB}','{/RB}');       
        $x=1;
//Conecting the database
        if (!$conn) {
        die('Not connected : ' . mysql_error());}


// Choose the current db
        mysql_select_db($db);

//Slurps the lexicon into an array
$sql = "SELECT word, score FROM concreteness";
$resultconcreteness = mysql_query($sql) or die(mysql_error());
$array = array();
while($row = mysql_fetch_assoc($resultconcreteness)) {
$array[] = $row;
}

//loop      
        while($x<=500000) {
        $data = mysql_query("SELECT `key`, `tagged` FROM speechesLCMcoded WHERE `key`='$x'") or die(mysql_error());

// puts the "data" info into the $info array 
        $info = mysql_fetch_array( $data);
        $tagged=$info['tagged'];
        unset($weight);
        unset($count);
        $weight=0;
        $count=0;

// Print out the contents of the entry 
        Print "<b>Key:</b> ".$info['key'] .  " <br>";

// Explodes the sentence
        $speech = explode(" ", $tagged);

// Loop every word  
        foreach($speech as $word) {

//Check if string contains our tag

if(!preg_match('/({V}|{J}|{N}|{RB})/', $word, $matches)) {} else{

//Removes our tags
        $word = str_replace($remove, "", $word);

        $id = searchForId($word, $array);
//      print "ID: " . $id . "<br>";
//      print "Word: " . $array[$id]['word'] . "<br>";
//      print "Score: " . $array[$id]['score'] . "<br>";
        $weight=$weight+$array[$id]['score'];
        $count=$count +1;
//      print "Weight: " . $weight . "<br>";
 //     print "Count: " . $count . "<br>";
        }
}
        $sql = "UPDATE speechesLCMcoded SET weight='$weight', count='$count' WHERE `key`='$x';" ;
        $retval = mysql_query( $sql, $conn );
        if(! $retval )
        {die('Could not update data: ' . mysql_error());}
        echo "Updated data successfully\n";
        ob_flush();
        flush();   

        //Increase the loop by one
        $x=$x+1;

}?>

这是索引:

CREATE TABLE `speechesLCMcoded` (
 `key` int(11) NOT NULL AUTO_INCREMENT,
 `speaker_state` varchar(100) NOT NULL,
 `speaker_first` varchar(100) NOT NULL,
 `congress` varchar(100) NOT NULL,
 `title` varchar(100) NOT NULL,
 `origin_url` varchar(100) NOT NULL,
 `number` varchar(100) NOT NULL,
 `order` varchar(100) NOT NULL,
 `volume` varchar(100) NOT NULL,
 `chamber` varchar(100) NOT NULL,
 `session` varchar(100) NOT NULL,
 `id` varchar(100) NOT NULL,
 `raw` mediumtext NOT NULL,
 `capitolwords_url` varchar(100) NOT NULL,
 `speaker_party` varchar(100) NOT NULL,
 `date` varchar(100) NOT NULL,
 `bills` varchar(100) NOT NULL,
 `bioguide_id` varchar(100) NOT NULL,
 `pages` varchar(100) NOT NULL,
 `speaker_last` varchar(100) NOT NULL,
 `speaker_raw` varchar(100) NOT NULL,
 `tagged` mediumtext NOT NULL,
 `adjectives` varchar(10) NOT NULL,
 `verbs` varchar(10) NOT NULL,
 `nouns` varchar(10) NOT NULL,
 `weight` varchar(50) NOT NULL,
 `count` varchar(50) NOT NULL,
 PRIMARY KEY (`key`)
) ENGINE=InnoDB AUTO_INCREMENT=408344 DEFAULT CHARSET=latin1

【问题讨论】:

  • 老实说,我认为 MySQL 是错误的技术。如果您想在数据分析期间进行高分,不妨看看 Lucene 或 Elasticsearch 之类的东西。
  • 感谢您的建议,但我现在无法承受更改技术的费用。我真的在寻找优化当前系统的方法。
  • 慢是一回事,但一个小时处理 500 行似乎真的非常慢。即每段大约 12 秒。我建议将 php 代码移动到存储过程中。在数据库中完成单个段落的工作应该会显示出相当大的性能改进。
  • 也许可以尝试记录您的查询,然后使用 MySQL EXPLAIN 命令分析它们。这将告诉您索引的任何问题以及如何优化它们。 dev.mysql.com/doc/refman/5.0/en/explain.html
  • 我添加了我的代码,如果它有助于理解什么是这么慢的话。

标签: php mysql sql lookup words


【解决方案1】:

您有一个相当小的参考表(您的词典)和一个巨大的文本语料库(表 1)。

如果我是你,我会通过将表中的整个词典放入内存中的 php 数组来启动你的程序。即使你所有的单词都是 20 个字符的长度,这也只需要十几兆字节的 RAM。

然后通过在内存中查找单词而不是使用 SQL 查询来执行第 4 步。您的内部循环(对于每个单词)会更快,并且同样准确。

不过,要小心一件事。如果要复制 MySQL 的不区分大小写的查找行为,则需要通过将它们转换为小写来规范化词典中的单词。

查看您的代码后进行编辑

一些专业提示:

  • 正确缩进您的代码,这样您就可以一目了然地看到循环的结构。
  • 请记住,将数据传递给函数需要时间。
  • PHP 数组是关联的。你可以做$value = $array[$key]。这很快。您不必线性搜索数组。你这样做每个单词一次 !!
  • 准备好的陈述很好。
  • 当您可以从结果集中读取下一行时重复 SQL 语句是不好的。
  • 流式传输结果集很好。
  • mysql_ 函数调用集已被其开发人员和其他所有人弃用和鄙视,这是有充分理由的。

你的循环中发生了太多事情。

你需要的是这个:

首先,从使用mysql_接口切换到使用mysqli_。去做就对了。 mysql_ 太慢、太老、太粗鲁了。

$db = new mysqli("host", "user", "password", "database");

其次,改变你加载词典的方式,优化整个关联数组dealio。

$lookup = array();
//Slurps the lexicon into an array, streaming it row by row
$sql = "SELECT word, score FROM concreteness";
$db->real_query($sql) || die $db->error;
$lkup = $db->use_result();
while ($row = $lkup->fetch_row()) {
      $lookup[strtolower($row[0])] = $row[1];
}
$lkup->close();

这为您提供了一个名为$lookup 的关联数组。如果你有一个$word,你可以通过这种方式找到它的权重值。这很快。您在示例代码中的内容非常慢。请注意,键在创建时和查找单词时都转换为小写。出于性能原因,如果可以避免,请不要将其放入函数中。

if (array_key_exists( strtolower($word), $lookup )) {
    $weight += $lookup[strtolower($word)]; /* accumulate weight */
    $count ++;                             /* increment count   */
}
else {
  /* the word was not found in your lexicon. handle as needed */
}

最后,您需要优化对文本语料库行的查询及其更新。我相信你应该使用准备好的语句来做到这一点。

接下来是这样的。

在您的程序开头附近,放置此代码。

$previouskey = -1;
if (/* you aren't starting at the beginning */) {
   $previouskey = /* the last successfully processed row */
}

$get_stmt = $db->prepare('SELECT `key`, `tagged` 
                           FROM speechesLCMcoded 
                          WHERE `key` > ?
                          ORDER BY `key` LIMIT 1' );

$post_stmt = $db->prepare ('UPDATE speechesLCMcoded 
                               SET weight=?, 
                                   count=? 
                             WHERE `key`=?' );

这些为您的处理提供了两个现成的语句。

请注意,$get_stmt 检索您尚未处理的第一个 key。即使您缺少一些密钥,这也将起作用。总是好的。这将非常有效,因为您的 key 列上有一个索引。

这就是你的循环最终的样子:

 $weight = 0;
 $count = 0;
 $key = 0;
 $tagged = '';

 /* bind parameters and results to the get statement */
 $get_stmt->bind_result($key, $tagged);
 $get_stmt->bind_param('i', $previouskey);

 /* bind parameters to the post statement */
 $post_stmt->bind_param('iii',$weight, $count, $key);

 $done = false;
 while ( !$done ) {
    $get_stmt->execute();
    if ($get_stmt->fetch()) {

        /* do everything word - by - word  here on the $tagged string */

        /* do the post statement to store the results */
        $post_stmt->execute();

        /* update the previous key prior to next iteration */
        $previouskey = $key; 
        $get_stmt->reset();
        $post_stmt->reset();
    } /* end if fetch */
    else {
       /* no result returned! we are done! */
       $done = true;
    }
 } /* end while not done */

这应该让您每行的处理时间达到亚秒级。

【讨论】:

  • 感谢您的回答。我试过了,从一行到另一行基本上需要 10 秒,这意味着大约需要 45 天的处理时间......
  • 我添加了我的代码,如果有帮助的话。我知道这很混乱......我是 PHP 新手。
  • 关于副本的说法不太正确,PHP 传递指针并且仅当您在函数范围内更改变量时才会创建副本。如果你传递一个对象,当然不是如果函数获得引用,这不是真的。如果您不更改函数中的变量,则不会创建副本。通过引用传递甚至比不通过引用传递还要慢,即使 PHP 必须在某些时候创建一个副本。
  • 整个算法可以单独使用 MySQL 完成,PHP 除了输入新段落之外不需要做任何事情。不要低估它,因为它确实在某种程度上回答了这个问题,但还有更好的选择。
  • 我的评论只是关于复制/参考/等的事实。我并不是说你的答案有任何错误。
【解决方案2】:

第一个明显的优化是这样的:

include "functions.php";
set_time_limit(0); // NOTE: no time limit
if (!$conn)
    die('Not connected : ' . mysql_error());
$remove = array('{J}','{/J}','{N}','{/N}','{V}','{/V}','{RB}','{/RB}'); // tags to remove       
$db = 'senate';
mysql_select_db($db);

$resultconcreteness = mysql_query('SELECT `word`, `score` FROM `concreteness`') or die(mysql_error());
$array = array(); // NOTE: init score cache
while($row = mysql_fetch_assoc($resultconcreteness))
    $array[strtolower($row['word'])] = $row['score']; // NOTE: php array as hashmap
mysql_free_result($resultconcreteness);

$data = mysql_query('SELECT `key`, `tagged` FROM `speechesLCMcoded`') or die(mysql_error()); // NOTE: single query instead of multiple
while ($row = mysql_fetch_assoc($data)) {
    $key = $row['key'];
    $tagged = $row['tagged'];
    $weight = $count = 0;
    $speech = explode(' ', $tagged);
    foreach ($speech as $word) {
        if (preg_match('/({V}|{J}|{N}|{RB})/', $word, $matches)) {
            $weight += $array[strtolower(str_replace($remove, '', $word))]; // NOTE: quick access to word's score
            $count++;
        }
    }
    mysql_query('UPDATE `speechesLCMcoded` SET `weight`='.$weight.', `count`='.$count.' WHERE `key`='.$key, $conn) or die(mysql_error());
}
mysql_free_result($data);

使用 检查 cmets:

但是对于 400K 行,这将需要一些时间,至少因为您必须更新每一行,这意味着 400K 更新。

未来可能的优化:

  1. 让这个脚本获取起始偏移量和长度等参数(将它们传递给 mysql LIMIT),这样您就可以同时运行多个脚本来处理不同的表块
  2. 而不是更新 - 使用数据生成文件,然后使用 LOAD DATA INFILE 替换您的表,400K 更新也可能更快

【讨论】:

    猜你喜欢
    • 2017-05-21
    • 1970-01-01
    • 1970-01-01
    • 2019-07-01
    • 1970-01-01
    • 1970-01-01
    • 2019-02-01
    • 2016-06-19
    • 2023-04-04
    相关资源
    最近更新 更多