waterfall 0.1.3__tar.gz → 0.1.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: waterfall
3
- Version: 0.1.3
3
+ Version: 0.1.6
4
4
  Summary: Scalable Framework for Robust Text Watermarking and Provenance for LLMs
5
5
  Project-URL: Homepage, https://github.com/aoi3142/Waterfall
6
6
  Project-URL: Issues, https://github.com/aoi3142/Waterfall/issues
@@ -141,7 +141,7 @@ from waterfall.watermark import verify_texts
141
141
  id = 1 # specify your watermarking ID
142
142
  test_texts = ["...", "..."] # Suspected texts to verify
143
143
 
144
- watermark_strength = verify_texts(test_texts, id)[0] # np array of floats
144
+ watermark_strength = verify_texts(test_texts, id) # np array of floats
145
145
  ```
146
146
 
147
147
  ## Code structure
@@ -121,7 +121,7 @@ from waterfall.watermark import verify_texts
121
121
  id = 1 # specify your watermarking ID
122
122
  test_texts = ["...", "..."] # Suspected texts to verify
123
123
 
124
- watermark_strength = verify_texts(test_texts, id)[0] # np array of floats
124
+ watermark_strength = verify_texts(test_texts, id) # np array of floats
125
125
  ```
126
126
 
127
127
  ## Code structure
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "waterfall"
7
- version = "0.1.3"
7
+ version = "0.1.6"
8
8
  authors = [
9
9
  { name = "Xinyuan Niu", email="aperture@outlook.sg" }
10
10
  ]
@@ -0,0 +1,129 @@
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": 1,
6
+ "id": "ddca610d",
7
+ "metadata": {},
8
+ "outputs": [],
9
+ "source": [
10
+ "from waterfall.watermark import *\n",
11
+ "import torch"
12
+ ]
13
+ },
14
+ {
15
+ "cell_type": "code",
16
+ "execution_count": 2,
17
+ "id": "f0b0a02a",
18
+ "metadata": {},
19
+ "outputs": [],
20
+ "source": [
21
+ "model_name_or_path = 'meta-llama/Llama-3.1-8B-Instruct'"
22
+ ]
23
+ },
24
+ {
25
+ "cell_type": "code",
26
+ "execution_count": 3,
27
+ "id": "ca5f7d55",
28
+ "metadata": {},
29
+ "outputs": [
30
+ {
31
+ "data": {
32
+ "application/vnd.jupyter.widget-view+json": {
33
+ "model_id": "2ef1e58174204e879a462a2a829d74fa",
34
+ "version_major": 2,
35
+ "version_minor": 0
36
+ },
37
+ "text/plain": [
38
+ "Loading checkpoint shards: 0%| | 0/4 [00:00<?, ?it/s]"
39
+ ]
40
+ },
41
+ "metadata": {},
42
+ "output_type": "display_data"
43
+ }
44
+ ],
45
+ "source": [
46
+ "\n",
47
+ "tokenizer = AutoTokenizer.from_pretrained(model_name_or_path)\n",
48
+ "model = AutoModelForCausalLM.from_pretrained(\n",
49
+ " model_name_or_path,\n",
50
+ " torch_dtype=torch.bfloat16,\n",
51
+ " device_map=\"cuda:5\",\n",
52
+ " )"
53
+ ]
54
+ },
55
+ {
56
+ "cell_type": "code",
57
+ "execution_count": 4,
58
+ "id": "2c475de5",
59
+ "metadata": {},
60
+ "outputs": [],
61
+ "source": [
62
+ "watermarker = Watermarker(tokenizer=tokenizer, model=model, id=1)"
63
+ ]
64
+ },
65
+ {
66
+ "cell_type": "code",
67
+ "execution_count": 5,
68
+ "id": "e74a58b5",
69
+ "metadata": {},
70
+ "outputs": [],
71
+ "source": [
72
+ "texts = [\"James is a scary man who loves to scare children\", \"There were 3 birds that ate the whole bowl\"]"
73
+ ]
74
+ },
75
+ {
76
+ "cell_type": "code",
77
+ "execution_count": 8,
78
+ "id": "fd0af01e",
79
+ "metadata": {},
80
+ "outputs": [],
81
+ "source": [
82
+ "watermarked_text = watermark_texts(texts, id, watermarker=watermarker)"
83
+ ]
84
+ },
85
+ {
86
+ "cell_type": "code",
87
+ "execution_count": 10,
88
+ "id": "4d58ec82",
89
+ "metadata": {},
90
+ "outputs": [
91
+ {
92
+ "data": {
93
+ "text/plain": [
94
+ "['James is an intimidating individual known for taking great pleasure in frightening kids, and this tendency often sends them running in terror.',\n",
95
+ " \"A small flock comprised of three birds consumed the entirety of the offered bowl's contents within a single meal session.\"]"
96
+ ]
97
+ },
98
+ "execution_count": 10,
99
+ "metadata": {},
100
+ "output_type": "execute_result"
101
+ }
102
+ ],
103
+ "source": [
104
+ "watermarked_text"
105
+ ]
106
+ }
107
+ ],
108
+ "metadata": {
109
+ "kernelspec": {
110
+ "display_name": "Python 3",
111
+ "language": "python",
112
+ "name": "python3"
113
+ },
114
+ "language_info": {
115
+ "codemirror_mode": {
116
+ "name": "ipython",
117
+ "version": 3
118
+ },
119
+ "file_extension": ".py",
120
+ "mimetype": "text/x-python",
121
+ "name": "python",
122
+ "nbconvert_exporter": "python",
123
+ "pygments_lexer": "ipython3",
124
+ "version": "3.12.9"
125
+ }
126
+ },
127
+ "nbformat": 4,
128
+ "nbformat_minor": 5
129
+ }
@@ -0,0 +1,118 @@
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": 49,
6
+ "id": "116f61a2",
7
+ "metadata": {},
8
+ "outputs": [],
9
+ "source": [
10
+ "import numpy as np\n",
11
+ "from tqdm import tqdm\n",
12
+ "from itertools import product, combinations\n",
13
+ "from scipy.stats import binom"
14
+ ]
15
+ },
16
+ {
17
+ "cell_type": "code",
18
+ "execution_count": 50,
19
+ "id": "53c66973",
20
+ "metadata": {},
21
+ "outputs": [
22
+ {
23
+ "data": {
24
+ "text/plain": [
25
+ "np.float64(0.38742048899999965)"
26
+ ]
27
+ },
28
+ "execution_count": 50,
29
+ "metadata": {},
30
+ "output_type": "execute_result"
31
+ }
32
+ ],
33
+ "source": [
34
+ "binom.pmf(1, 10, 0.1)"
35
+ ]
36
+ },
37
+ {
38
+ "cell_type": "code",
39
+ "execution_count": 56,
40
+ "metadata": {},
41
+ "outputs": [
42
+ {
43
+ "name": "stderr",
44
+ "output_type": "stream",
45
+ "text": [
46
+ "100%|██████████| 32/32 [00:00<00:00, 10845.88it/s]\n",
47
+ "100%|██████████| 32/32 [00:00<00:00, 3035.09it/s]\n",
48
+ "100%|██████████| 32/32 [00:00<00:00, 1622.42it/s]\n",
49
+ "100%|██████████| 32/32 [00:00<00:00, 2020.93it/s]\n",
50
+ "100%|██████████| 32/32 [00:00<00:00, 4749.72it/s]"
51
+ ]
52
+ },
53
+ {
54
+ "name": "stdout",
55
+ "output_type": "stream",
56
+ "text": [
57
+ "0.6582226332676197\n"
58
+ ]
59
+ },
60
+ {
61
+ "name": "stderr",
62
+ "output_type": "stream",
63
+ "text": [
64
+ "\n"
65
+ ]
66
+ }
67
+ ],
68
+ "source": [
69
+ "n = 5 # n-gram\n",
70
+ "# k = 1 # no of bits modified\n",
71
+ "l = 8 # bits of watermark\n",
72
+ "p = 0.1 # probability of modification\n",
73
+ "\n",
74
+ "result = 0\n",
75
+ "for k in range(0, n + 1):\n",
76
+ " binomial_prob = binom.pmf(k, n, p)\n",
77
+ " if k == 0:\n",
78
+ " result += binomial_prob * 1\n",
79
+ " continue\n",
80
+ " results = []\n",
81
+ "\n",
82
+ " original = np.zeros(n, dtype=bool)\n",
83
+ " for original in tqdm(product([True, False], repeat=n), total=2**n):\n",
84
+ " original = np.array(original, dtype=bool)\n",
85
+ " original_agreement = original.mean() > 0.5\n",
86
+ " for modified_indices in combinations(range(n), k):\n",
87
+ " modified = original.copy()\n",
88
+ " for i in product([True, False], repeat=k):\n",
89
+ " modified[np.array(modified_indices)] = i\n",
90
+ " modified_agreement = modified.mean() > 0.5\n",
91
+ " results.append(original_agreement == modified_agreement)\n",
92
+ " result += binomial_prob * np.mean(results) ** l\n",
93
+ "print(result)"
94
+ ]
95
+ }
96
+ ],
97
+ "metadata": {
98
+ "kernelspec": {
99
+ "display_name": "Python 3",
100
+ "language": "python",
101
+ "name": "python3"
102
+ },
103
+ "language_info": {
104
+ "codemirror_mode": {
105
+ "name": "ipython",
106
+ "version": 3
107
+ },
108
+ "file_extension": ".py",
109
+ "mimetype": "text/x-python",
110
+ "name": "python",
111
+ "nbconvert_exporter": "python",
112
+ "pygments_lexer": "ipython3",
113
+ "version": "3.12.9"
114
+ }
115
+ },
116
+ "nbformat": 4,
117
+ "nbformat_minor": 5
118
+ }
@@ -141,6 +141,7 @@ class Watermarker:
141
141
  do_sample=do_sample,
142
142
  logits_processor=logits_processor,
143
143
  pad_token_id=self.tokenizer.eos_token_id,
144
+ tokenizer=self.tokenizer,
144
145
  **kwargs
145
146
  )
146
147
  output = output[:,tokd_input["input_ids"].shape[-1]:].cpu()
@@ -1,10 +1,13 @@
1
1
  import argparse
2
2
  import logging
3
3
  import os
4
+ import gc
4
5
  import torch
6
+ import numpy as np
5
7
  from typing import List, Literal, Optional, Tuple
6
8
 
7
9
  from transformers import AutoTokenizer, AutoModelForCausalLM
10
+ from transformers.modeling_utils import PreTrainedModel
8
11
  from sentence_transformers import SentenceTransformer
9
12
  from tqdm.auto import tqdm
10
13
 
@@ -21,6 +24,8 @@ PROMPT = (
21
24
  )
22
25
  PRE_PARAPHRASED = "Here is a paraphrased version of the text while preserving the semantic similarity:\n\n"
23
26
 
27
+ waterfall_cached_watermarking_model = None # Global variable to cache the watermarking model
28
+
24
29
  def detect_gpu() -> str:
25
30
  """
26
31
  Use torch to detect if MPS, CUDA, or neither (default CPU)
@@ -42,9 +47,10 @@ def watermark(
42
47
  sts_model: SentenceTransformer,
43
48
  num_beam_groups: int = 4,
44
49
  beams_per_group: int = 2,
45
- STS_scale:float = 2.0,
50
+ STS_scale: float = 2.0,
46
51
  diversity_penalty: float = 0.5,
47
52
  max_new_tokens: Optional[int] = None,
53
+ **kwargs
48
54
  ) -> str:
49
55
  paraphrasing_prompt = watermarker.tokenizer.apply_chat_template(
50
56
  [
@@ -61,6 +67,7 @@ def watermark(
61
67
  num_beam_groups = num_beam_groups,
62
68
  num_return_sequences = num_beam_groups * beams_per_group,
63
69
  diversity_penalty = diversity_penalty,
70
+ **kwargs,
64
71
  )
65
72
 
66
73
  # Select best paraphrasing based on q_score and semantic similarity
@@ -75,8 +82,9 @@ def watermark(
75
82
  def verify_texts(texts: List[str], id: int,
76
83
  watermarker: Optional[Watermarker] = None,
77
84
  k_p: Optional[int] = None,
78
- model_path: Optional[str] = "meta-llama/Llama-3.1-8B-Instruct"
79
- ) -> Tuple[float,float]:
85
+ model_path: Optional[str] = "meta-llama/Llama-3.1-8B-Instruct",
86
+ return_extracted_k_p: bool = False
87
+ ) -> np.ndarray | Tuple[np.ndarray,np.ndarray]:
80
88
  """Returns the q_score and extracted k_p"""
81
89
 
82
90
  if watermarker is None:
@@ -87,7 +95,11 @@ def verify_texts(texts: List[str], id: int,
87
95
  if k_p is None:
88
96
  k_p = watermarker.k_p
89
97
 
90
- verify_results = watermarker.verify(texts, id=[id], k_p=[k_p], return_extracted_k_p=True) # results are [text x id x k_p]
98
+ verify_results = watermarker.verify(texts, id=[id], k_p=[k_p], return_extracted_k_p=return_extracted_k_p) # results are [text x id x k_p]
99
+
100
+ if not return_extracted_k_p:
101
+ return verify_results[:,0,0]
102
+
91
103
  q_score = verify_results["q_score"]
92
104
  k_p_extracted = verify_results["k_p_extracted"]
93
105
 
@@ -140,6 +152,7 @@ def watermark_texts(
140
152
  diversity_penalty: float = 0.5,
141
153
  STS_scale:float = 2.0,
142
154
  use_tqdm: bool = False,
155
+ stop_at_double_newline: bool = True, # if True, will stop generation at the first double newline. Prevent repeated paraphrasing of the same text.
143
156
  ) -> List[str]:
144
157
  if watermark_fn == 'fourier':
145
158
  watermarkingFnClass = WatermarkingFnFourier
@@ -150,11 +163,24 @@ def watermark_texts(
150
163
 
151
164
  if watermarker is None:
152
165
  assert model_path is not None, "model_path must be provided if watermarker is not passed"
153
- model = AutoModelForCausalLM.from_pretrained(
154
- model_path,
155
- torch_dtype=torch_dtype,
156
- device_map=device,
157
- )
166
+ global waterfall_cached_watermarking_model
167
+
168
+ if isinstance(waterfall_cached_watermarking_model, PreTrainedModel) and waterfall_cached_watermarking_model.name_or_path != model_path:
169
+ device = waterfall_cached_watermarking_model.device.type
170
+ waterfall_cached_watermarking_model = None
171
+ gc.collect()
172
+ if device == "cuda":
173
+ torch.cuda.empty_cache()
174
+ elif device == "mps":
175
+ torch.mps.empty_cache()
176
+
177
+ if waterfall_cached_watermarking_model is None:
178
+ waterfall_cached_watermarking_model = AutoModelForCausalLM.from_pretrained(
179
+ model_path,
180
+ torch_dtype=torch_dtype,
181
+ device_map=device,
182
+ )
183
+ model = waterfall_cached_watermarking_model
158
184
  tokenizer = AutoTokenizer.from_pretrained(model_path)
159
185
 
160
186
  watermarker = Watermarker(tokenizer=tokenizer, model=model, id=id, kappa=kappa, k_p=k_p, watermarkingFnClass=watermarkingFnClass)
@@ -173,6 +199,9 @@ def watermark_texts(
173
199
  T_ws = []
174
200
 
175
201
  for T_o in tqdm(T_os, desc="Watermarking texts", disable=not use_tqdm):
202
+ if stop_at_double_newline and "\n\n" in T_o:
203
+ logging.warning("Text contains \\n\\n and stop_at_double_newline is set to True, replacing all \\n\\n in text.")
204
+ T_o = T_o.replace("\n\n", " ") # replace double newlines with space
176
205
  T_w = watermark(
177
206
  T_o,
178
207
  watermarker = watermarker,
@@ -181,6 +210,7 @@ def watermark_texts(
181
210
  beams_per_group = beams_per_group,
182
211
  diversity_penalty = diversity_penalty,
183
212
  STS_scale = STS_scale,
213
+ stop_strings=["\n\n"] if stop_at_double_newline else None,
184
214
  )
185
215
  T_ws.append(T_w)
186
216
 
@@ -283,7 +313,7 @@ def main():
283
313
  )
284
314
 
285
315
  # watermarker = Watermarker(tokenizer=tokenizer, model=None, id=id, k_p=k_p, watermarkingFnClass=watermarkingFnClass) # If only verifying the watermark, do not need to instantiate the model
286
- q_scores, extracted_k_ps = verify_texts(T_os + T_ws, id, watermarker, k_p=k_p)
316
+ q_scores, extracted_k_ps = verify_texts(T_os + T_ws, id, watermarker, k_p=k_p, return_extracted_k_p=True)
287
317
 
288
318
  for i in range(len(T_os)):
289
319
  # Handle the case where this is being run
File without changes
File without changes
File without changes
File without changes