Zin opsplitsen in "deelproblemen" ivm vertaling

Overzicht Reageren

Sponsored by: Vacatures door Monsterboard

Rob Doemaarwat

Rob Doemaarwat

21/08/2020 19:10:27
Quote Anchor link
Voor artikelen in een webshop krijg ik vanuit verschillende kanalen data aangeleverd. Een deel van die data is op basis van vaste kenmerken (merk, prijs, enz). Daarnaast is er altijd nog het afvoerputje "opmerkingen". Hierin staan enerzijds de opmerkingen die de leverancier zelf heeft ingevoerd, en anderzijds kenmerken die aan onze kant (of halverwege het "doorgeef" proces) geen eigen veld hebben. Een opmerking kan dus bijvoorbeeld iets zijn van "Foo Bar 1.6 / Past ook op Noot Mies 1.6 / Let op: krassen aan achterzijde / OEM 123ABC456". Voor de goede orde heb ik het geheel nog een beetje gescheiden dmv slashes, maar meestal is het gewoon een grote brei aan "tekst" (en soms ook nog in meerdere talen). Nou willen we deze tekst automatisch gaan vertalen (Google / Azure / Yandex Translate), maar daar betaal je per te vertalen karakter, en het gaat om heel veel artikelen, en om een flink aantal talen = hoge rekening als je alles maar klakkeloos door de vertaalmolen draait ...

Een groot deel van deze "opmerkingen" is echter "algemene bagger" die niet vertaald hoeft/kan worden. Zo zijn "Foo Bar" en "Noot Mies" bijvoorbeeld merken (we hebben een lijst met merken), en die blijven natuurlijk gelijk. "1.6" is ook niet iets wat in het Engels heel anders zal worden (beter: ook niet moet worden). Een tekst als "Past ook op" komt regelmatig voor en hoeft dus niet elke keer vertaald te worden. Kortom: in bovenstaand voorbeeld zou dus eigenlijk alleen "krassen aan achterzijde" vertaald moeten worden.

Ik heb al een analyse methode om "algemene zinsdelen" er uit te halen (zoals "Past ook op"). Ook merken en nummers zijn eenvoudig te "detecteren". Kortom: ik ben al zover dat ik "weet" dat ik enkel nog "krassen aan achterzijde" hoef te vertalen. Het probleem is nu: hoe ga ik dit handig doen. Ik zit nu op de toer waarbij ik delen van de zin markeer (dmv "markers") als bijvoorbeeld zijnde "zo laten" (merken, nummers), "standaardzin 21", enz. Tijdens het vertalen (per doel-taal) kan ik dan de "zo laten" delen ... zo laten, en voor de standaardzinnen de juiste "reeds vertaalde" zinsneden ophalen (in de juiste taal). Maar ik doe dit letterlijk "in de zin". Bovenstaande zin wordt dus iets van "<~~Foo Bar~~> <~~1.6~~> / <~~algemeen=21~~> <~~Noot Mies~~> <~~1.6~~> / <~~algemeen=41~~> krassen aan achterzijde / <~~algemeen=28~~> <~~123ABC456~~>". Kortom: alles tussen <~~ ... ~~> is iets wat ik lokaal kan "vertalen" (of niet hoef te vertalen), en alleen het stukje "krassen aan achterzijde" hoeft naar de vertaalservice (23 ipv 89 karakters; in mijn analyse kwam ik zelfs tot 11% = 89% kosten reductie). Dit is veelal een regex gebeuren (bijvoorbeeld preg_replace('/\\b(\\w*\\d\\w*)\\b/', '<~~$1~~>', $str) om "nummers" tussen markers te krijgen).

Alleen: dit kraakt. Het voelt alsof je een beetje met een hamer net zolang ergens op staat te rammen tot het de goede vorm heeft. Niet bepaald subtiel dus (en het is natuurlijk wachten op de eerste opmerking met "<~~" er in ...). Het liefst zou ik de zin in een array splitsen met dan per zinsdeel de bijbehorende "methode":
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$sentence = [
  ['str' => 'Foo Bar', 'type' => 'make'],
  ['str' => '1.6', 'type' => 'number'],
  ['str' => ' / ', 'type' => 'other'],
  ['str' => 'Past ook op ', 'type' => 'common', 'id' => 21'],
  ['str' => 'Noot Mies', 'type' => 'make'],
  ['str' => '1.6', 'type' => 'number'],
  ['str' => ' / ', 'type' => 'other'],
  ['str' => 'Let op: ', 'type' => 'common', 'id' => 41],
  ['str' => 'krassen aan achterzijde', 'type' => 'trans'],
  ['str' => ' / ', 'type' => 'other'],
  ['str' => 'OEM', 'type' => 'common', 'id' => 28],
  ['str' => '123ABC456', 'type' => 'number']
];


Vervolgens is het een kwestie van per taal de array doorlopen, en per type zinsdeel de juiste actie ondernemen (en vervolgens de boel weer aan elkaar plakken en ergens opslaan). Een stuk mooier dus.

Vraag is nu: hoe ga ik de originele zin zo mooi "tokenizen" op basis van alle verschillende "type" zinsdeel? Ik kan natuurlijk het "search & replace" resultaat wat ik nu al heb gaan splitsen op de markers, maar dan blijft dat "slaan met de hamer" gevoel hangen. Het liefst zou ik de zin direct in bovenstaande mootjes hakken.
 
PHP hulp

PHP hulp

21/11/2024 20:34:43
 
Ward van der Put
Moderator

Ward van der Put

21/08/2020 19:38:42
Quote Anchor link
Een translation memory (TM) is daarvoor een oplossing. Daarmee hoef je eerder vertaalde strings niet opnieuw te vertalen én kun je permanent menselijke corecties van machinevertaalfouten vastleggen.

Als je avontuurlijk bent aangelegd, lijkt me dit een ideaal experiment om de mogelijkheden van machine learning eens stevig aan de tand te voelen. ;)
 
Thomas van den Heuvel

Thomas van den Heuvel

21/08/2020 20:24:57
Quote Anchor link
Maar propageer je hiermee niet het probleem? Je hebt een onsamenhangende betekenisloze brei die je wilt vertalen naar... een andere onsamenhangende betekenisloze brei?

Zou je niet op een andere manier mening/betekenis kunnen geven aan deze informatie zodat deze min of meer in "vakjes" past? Dus in de vorm van eigenschappen en bijbehorende waarden? Je hoeft dan "enkel" de labels te vertalen.

Heb je al eens bij wijze van experiment gekeken of je dit in een soort van indeling kunt gieten met tags oid?

Misschien zou je dit ook deels aan de koppeling-kant kunnen oplossen? Wellicht als je meer ruimte biedt voor invoer in plaats van de eerder genoemde afvoerput komt deze informatie misschien beter tot zijn recht? Kunnen er afspraken gemaakt worden over het meer standaardiseren van het formaat van aangeleverde informatie? Anders blijft het toch een beetje shit out/shit in.

Daarnaast zou je kunnen kijken welke informatie relevant is en welke niet. Als de informatie relevant is dan zou ik zeggen dat een eigen plekje gerechtvaardigd is en als deze niet relevant is waarom zou je dan moeite doen om deze op te slaan en/of te vertalen?

Je bent nu vooral bezig met de vraag "hoe ga ik dit aanpakken", maar hoe zit het met de vragen "heb ik deze informatie nodig", "hoe wordt deze (vervolgens) ingezet/gebruikt" en "is dit de enige/beste/eenvoudigste aanpak die leidt tot het gewenste eindresultaat"?

Mogelijk probeer je ook iets te hard een machine te laten doen waar een persoon mogelijk beter in is, het gaat namelijk ook over de interpretatie van informatie. Je zou dit werk in principe ook, I don't know, door een stagiair kunnen laten doen ofzo, om maar een dwarsstraat te noemen.
 
Rob Doemaarwat

Rob Doemaarwat

21/08/2020 20:51:56
Quote Anchor link
@Thomas,

Ik hang aan het einde van het afvoerputje. De data komt vanuit heel Europa, en vaak over verschillende schijven. Hoe vaak we nu al moeten "vechten" om heel basale dingen in het juiste vakje te krijgen ... Dat ga ik niet redden om alles op de juiste plek te krijgen. Dat is wat mij betreft dus een gepasseerd station.

Ook gaat het om veel te veel data om "met het handje" te doen. Het gaat om miljoenen records, met elke dag duizenden updates. De kwaliteit van de vertaling is ook niet zo heel belangrijk. Als maar "enigszins" duidelijk is wat er staat (beter een kromme Nederlandse zin, dan de originele - ik noem maar een dwarsstraat - Portugese tekst).

Mbt dat laatste ook nog de afweging Yandex (goedkoop / slechte vertaling) / Azure (medium / redelijk) / Google (duur / goed). Tussen Yandex en Google zit een factor 4. Dat tikt ook weer aardig aan ...

@Ward,

Ah, fijn om te zien dat ik een bestaand wiel (deels) aan het opnieuw uitvinden ben. TM is inderdaad wat ik aan het doen ben (om kosten te besparen). Vraag blijft dan: hoe ga ik de tekst "knap" in segmenten verdelen. Als je een ML tip hebt wil ik daar wel eens naar kijken, maar ik heb het idee dat het geheel nu zo basaal is (vaste lijsten) dat dat misschien een beetje over de top is (en anders heb ik het zelf al "uitgevonden" met m'n "algemene zinsdelen" opsporing).
 
Thomas van den Heuvel

Thomas van den Heuvel

21/08/2020 20:58:44
Quote Anchor link
Maar hoe belangrijk is deze informatie? Is het echt de moeite waard om te proberen recht te buigen wat in wezen krom is?
 
Rob Doemaarwat

Rob Doemaarwat

21/08/2020 21:20:47
Quote Anchor link
Niet alles is altijd van belang ... maar soms wel. Soms is de informatie "ter info" (bijvoorbeeld ook al duidelijk te zien op de foto's, of duplicaat van wat in de algemene "vakjes" staat), maar soms gaat het ook om "belangrijke" informatie (het gaat om artikelen die niet altijd "in nieuwstaat" zijn, dus dan is het wel handig om te weten als er delen ontbreken / beschadigd zijn, of anderszins niet "aan de verwachtingen" kunnen voldoen).
 
Ward van der Put
Moderator

Ward van der Put

22/08/2020 12:25:20
Quote Anchor link
Rob Doemaarwat op 21/08/2020 20:51:56:
Mbt dat laatste ook nog de afweging Yandex (goedkoop / slechte vertaling) / Azure (medium / redelijk) / Google (duur / goed). Tussen Yandex en Google zit een factor 4. Dat tikt ook weer aardig aan ...


Er zijn er nog meer. Bijvoorbeeld DeepL schijnt goed te zijn.

Rob Doemaarwat op 21/08/2020 20:51:56:
Ah, fijn om te zien dat ik een bestaand wiel (deels) aan het opnieuw uitvinden ben. TM is inderdaad wat ik aan het doen ben (om kosten te besparen). Vraag blijft dan: hoe ga ik de tekst "knap" in segmenten verdelen.


Ik denk dat je dan eerst moet gaan segmenteren aan de hand van wat je al weet. Bijvoorbeeld voor elektronica geldt een andere "woordenschat" dan voor damesschoenen. Omdat ook automatisch vertalen tegenwoordig vaak ergens een vorm van machine learning gebruikt, helpt het als je daarvoor gescheiden datasets kunt aanleveren.

Ten tweede zou ik kijken hoe je prioriteiten kunt formaliseren. Sommige productgegevens zijn van levensbelang, en dat soms zelfs letterlijk, andere zijn verwaarloosbare marketese. Wat wil je weten en wat kun je vergeten? Als je de ruis kunt kunt wegfilteren, is wat je overhoudt beter te behappen. Kijk daarvoor bijvoorbeeld naar de productattributen in Schema.org, want die overlappen wat Google graag wil weten over producten. Je kunt daarmee clusters in de woordenschat vormen: sommige attributen hebben te maken met kleur, andere met het gewicht, enzovoort.
 
Rob Doemaarwat

Rob Doemaarwat

22/08/2020 14:57:19
Quote Anchor link
DeepL ziet er inderdaad goed uit, maar zit op € 20,-/miljoen karakters (en nog een kleine maandelijkse fee). Dat is gelijk aan Google Translate (en ondanks dat die ook "beter" is dan Yandex en Azure heb ik 'm toch al afgeschoten ivm "te duur").

Het is niet zo dat alles in de "opmerkingen" staat. Het meeste spul wat (bijvoorbeeld) van belang is voor de "structured data" voor Google staat gewoon netjes "in vakjes". Het zijn echt van die "losse flodders" waar je op voorhand ook niet altijd "een vakje" voor kunt verzinnen.

Ik heb vanmorgen even lopen klungelen, en voorlopig heb ik deze - toch vrij simpele / recht-toe-recht-aan "tokenizer":
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<?php

namespace DoeMaarWat;

/**
 *  String helpers.
 */

class Str{

//... other functions, constants, ...

  /**
   * Tokenize a string according to the rules.
   * @param string $str
   * @param array $rules  Array with records containig the rule type and extra parameters for the rule. See TOKEN_TYPE_*
   *   constants.
   * @return array  String divided into tokens (records), with 'type', 'str' (original string), rule parameters, and other info.
   */

  public static function tokenize($str,$rules){
    static $delimiter = null;
    static $indicator = null;
    if(!$delimiter) $delimiter = strrev($indicator = self::random(self::TOKEN_INDICATOR_LENGTH,'[a-z]'));
    $rule = array_shift($rules);
    $tokens = [];
    switch($type = $rule[self::TOKEN_TYPE] ?? null){
      case
self::TOKEN_TYPE_LIST:
        foreach($rule[$type] as $key => $item) $str = preg_replace(
          '/\\b' . preg_quote($item,'/') . '\\b/' . ($rule['case'] ?? false ? '' : 'i'),
          $delimiter . $indicator . $key . '=$0' . $delimiter,
          $str
        );
        foreach(explode($delimiter,$str) as $str) $tokens[] = substr($str,0,self::TOKEN_INDICATOR_LENGTH) == $indicator
          ? array_combine(['key',self::TOKEN_STR],explode('=',substr($str,self::TOKEN_INDICATOR_LENGTH),2)) + $rule
          : [self::TOKEN_TYPE => self::TOKEN_TYPE_OTHER,self::TOKEN_STR => $str];
        break;
      case
self::TOKEN_TYPE_REGEX:
        $str = preg_replace($rule[$type],$delimiter . $indicator . '$0' . $delimiter,$str);
        foreach(explode($delimiter,$str) as $str) $tokens[] = substr($str,0,self::TOKEN_INDICATOR_LENGTH) == $indicator
          ? [self::TOKEN_STR => substr($str,self::TOKEN_INDICATOR_LENGTH)] + $rule
          : [self::TOKEN_TYPE => self::TOKEN_TYPE_OTHER,self::TOKEN_STR => $str];
        break;
      default:

        throw new \Exception("Unknown rule type '$type'");
    }

    for($i = count($tokens) - 1; $i >= 0; $i--) if(($token = $tokens[$i])[self::TOKEN_TYPE] == self::TOKEN_TYPE_OTHER){
      if(!strlen($token[self::TOKEN_STR])) array_splice($tokens,$i,1); //remove empty
      elseif($rules) array_splice($tokens,$i,1,self::tokenize($token[self::TOKEN_STR],$rules));
    }

    return $tokens;
  }

}


?>
En met m'n voorbeeld wordt dat dan:
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
10
11
12
<?php

$str
='Foo Bar 1.6 / Past ook op Noot Mies 1.6 / Let op: krassen aan achterzijde / OEM 123ABC456';
print("$str\n\n");
var_dump(\DoeMaarWat\Str::tokenize($str,[
  [
'type' => 'list','list' => [5 => 'Hello World',123 => 'foo bar',124 => 'noot mies'],'name' => 'brand'],
  [
'type' => 'list','list' => [21 => 'past ook op',41 => 'let op: ',28 => 'OEM'],'name' => 'common'],
  [
'type' => 'regex','regex' => '/\\b\\S*\\d\\S*\\b/','name' => 'number'],
  [
'type' => 'regex','regex' => '/\\s*\\/\\s*/','name' => 'delimiter']
]));


?>
En dat levert dan (de "lijsten" even verwijderd):
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
Foo Bar 1.6 / Past ook op Noot Mies 1.6 / Let op: krassen aan achterzijde / OEM 123ABC456

array(16) {
  [0] => array(5) {
    ["key"] => string(3) "123"
    ["str"] => string(7) "Foo Bar"
    ["type"] => string(4) "list"
    ["list"] => array(3) {...}
    ["name"] => string(5) "brand"
  }
  [1] => array(2) {
    ["type"] => string(5) "other"
    ["str"] => string(1) " "
  }
  [2] => array(4) {
    ["str"] => string(3) "1.6"
    ["type"] => string(5) "regex"
    ["regex"] => string(14) "/\b\S*\d\S*\b/"
    ["name"] => string(6) "number"
  }
  [3] => array(4) {
    ["str"] => string(3) " / "
    ["type"] => string(5) "regex"
    ["regex"] => string(10) "/\s*\/\s*/"
    ["name"] => string(9) "delimiter"
  }
  [4] => array(5) {
    ["key"] => string(2) "21"
    ["str"] => string(11) "Past ook op"
    ["type"] => string(4) "list"
    ["list"] => array(3) {...}
    ["name"] => string(6) "common"
  }
  [5] => array(2) {
    ["type"] => string(5) "other"
    ["str"] => string(1) " "
  }
  [6] => array(5) {
    ["key"] => string(3) "124"
    ["str"] => string(9) "Noot Mies"
    ["type"] => string(4) "list"
    ["list"] => array(3) {...}
    ["name"] => string(5) "brand"
  }
  [7] => array(2) {
    ["type"] => string(5) "other"
    ["str"] => string(1) " "
  }
  [8] => array(4) {
    ["str"] => string(3) "1.6"
    ["type"] => string(5) "regex"
    ["regex"] => string(14) "/\b\S*\d\S*\b/"
    ["name"] => string(6) "number"
  }
  [9] => array(4) {
    ["str"] => string(3) " / "
    ["type"] => string(5) "regex"
    ["regex"] => string(10) "/\s*\/\s*/"
    ["name"] => string(9) "delimiter"
  }
  [10] => array(5) {
    ["key"] => string(2) "41"
    ["str"] => string(8) "Let op: "
    ["type"] => string(4) "list"
    ["list"] => array(3) {...}
    ["name"] => string(6) "common"
  }
  [11] => array(2) {
    ["type"] => string(5) "other"
    ["str"] => string(23) "krassen aan achterzijde"
  }
  [12] => array(4) {
    ["str"] => string(3) " / "
    ["type"] => string(5) "regex"
    ["regex"] => string(10) "/\s*\/\s*/"
    ["name"] => string(9) "delimiter"
  }
  [13] => array(5) {
    ["key"] => string(2) "28"
    ["str"] => string(3) "OEM"
    ["type"] => string(4) "list"
    ["list"] => array(3) {...}
    ["name"] => string(6) "common"
  }
  [14] => array(2) {
    ["type"] => string(5) "other"
    ["str"] => string(1) " "
  }
  [15] => array(4) {
    ["str"] => string(9) "123ABC456"
    ["type"] => string(5) "regex"
    ["regex"] => string(14) "/\b\S*\d\S*\b/"
    ["name"] => string(6) "number"
  }
}

Alle niet-whitspace "other" entries moet ik hierna dus (nog) vertalen (en analyseren op nieuwe "standaard teksten").
Gewijzigd op 22/08/2020 14:57:41 door Rob Doemaarwat
 
Rob Doemaarwat

Rob Doemaarwat

24/08/2020 19:33:34
Quote Anchor link
Voor de geïnteresseerden: nu zonder magic placeholders en preg_replace() (dat gaf bij de eerste test-run al problemen ...), maar met preg_match_all() en dan direct "opknippen":
Code (php)
PHP script in nieuw venster Selecteer het PHP script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<?php


  protected static function tokenizeMatches($str,$matches,$extra){
    $index = 0;
    $tokens = [];
    foreach($matches[0] as list($match,$offset)){
      $tokens[] = [self::TOKEN_TYPE => self::TOKEN_TYPE_OTHER,self::TOKEN_STR => substr($str,$index,$offset - $index)];
      $tokens[] = [self::TOKEN_STR => $match] + $extra;
      $index = $offset + strlen($match);
    }

    $tokens[] = [self::TOKEN_TYPE => self::TOKEN_TYPE_OTHER,self::TOKEN_STR => substr($str,$index)];
    return $tokens;
  }

  /**
   * Tokenize a string according to the rules.
   * @param string $str
   * @param array $rules  Array with records containig the rule type and extra parameters for the rule. See TOKEN_TYPE_*
   *   constants. The first rule is applied first. The next rule is applied to all parts of the string not matching the first
   *   rule, and so on.
   * @return array  String divided into tokens (records), with 'str' (matched part of string), 'rule' name, rule 'type', and
   *   other rule based info.
   */

  public static function tokenize($str,$rules){
    $extra = [
      self::TOKEN_RULE => $key = Record::key($rules),
      self::TOKEN_TYPE => $type = ($rule = $rules[$key])[self::TOKEN_TYPE]
    ];

    unset($rules[$key]);
    $tokens = [];
    switch($type){
      case
self::TOKEN_TYPE_LIST:
        $tokens[] = [self::TOKEN_TYPE => self::TOKEN_TYPE_OTHER,self::TOKEN_STR => $str];
        $length = strlen($str);
        $prefix = '/\\b';
        $suffix = '\\b/' . (($rule['case'] ?? false) ? '' : 'i');
        foreach($rule[$type] as $key => $item) if(($item_length = strlen($item)) <= $length) for($i = count($tokens) - 1; $i >= 0; $i--) if(
          ((
$token = $tokens[$i])[self::TOKEN_TYPE] == self::TOKEN_TYPE_OTHER) &&
          (
strlen($str = $token[self::TOKEN_STR]) >= $item_length) &&
          preg_match_all($prefix . preg_quote($item,'/') . $suffix,$str,$matches,PREG_OFFSET_CAPTURE)
        )
array_splice($tokens,$i,1,self::tokenizeMatches($str,$matches,compact('key') + $extra));
        break;
      case
self::TOKEN_TYPE_REGEX:
        preg_match_all($rule[$type],$str,$matches,PREG_OFFSET_CAPTURE);
        $tokens = self::tokenizeMatches($str,$matches,$extra);
        break;
      case
self::TOKEN_TYPE_FUNC:
        foreach(call_user_func($rule[$type],$str,$rule) as $token)
          $tokens[] = $token + (($token[self::TOKEN_TYPE] ?? null) != self::TOKEN_TYPE_OTHER) ? $extra : [];
        break;
      default:

        throw new \Exception("Unknown rule type '$type'");
    }

    for($i = count($tokens) - 1; $i >= 0; $i--) if(($token = $tokens[$i])[self::TOKEN_TYPE] == self::TOKEN_TYPE_OTHER){
      if(!strlen($token[self::TOKEN_STR])) array_splice($tokens,$i,1); //remove empty
      elseif($rules) array_splice($tokens,$i,1,self::tokenize($token[self::TOKEN_STR],$rules));
    }

    return $tokens;
  }


?>
 



Overzicht Reageren

 
 

Om de gebruiksvriendelijkheid van onze website en diensten te optimaliseren maken wij gebruik van cookies. Deze cookies gebruiken wij voor functionaliteiten, analytische gegevens en marketing doeleinden. U vindt meer informatie in onze privacy statement.